diff --git a/.dockerignore b/.dockerignore index 1b080bdb9e3e..b6687c95fa7a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,6 +12,7 @@ docs/ **/output/* **/outputs/* **/videos/* +**/wandb/* *.tmp # ignore docker docker/cluster/exports/ @@ -25,3 +26,5 @@ recordings/ _isaac_sim? # Docker history docker/.isaac-lab-docker-history +# ignore uv environment +env_isaaclab diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 9b4a023d685b..000000000000 --- a/.flake8 +++ /dev/null @@ -1,23 +0,0 @@ -[flake8] -show-source=True -statistics=True -per-file-ignores=*/__init__.py:F401 -# E402: Module level import not at top of file -# E501: Line too long -# W503: Line break before binary operator -# E203: Whitespace before ':' -> conflicts with black -# D401: First line should be in imperative mood -# R504: Unnecessary variable assignment before return statement. -# R505: Unnecessary elif after return statement -# SIM102: Use a single if-statement instead of nested if-statements -# SIM117: Merge with statements for context managers that have same scope. -# SIM118: Checks for key-existence checks against dict.keys() calls. -ignore=E402,E501,W503,E203,D401,R504,R505,SIM102,SIM117,SIM118 -max-line-length = 120 -max-complexity = 30 -exclude=_*,.vscode,.git,docs/** -# docstrings -docstring-convention=google -# annotations -suppress-none-returning=True -allow-star-arg-any=True diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md index 9946d02165da..c07d7f56dc85 100644 --- a/.github/ISSUE_TEMPLATE/proposal.md +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -21,6 +21,14 @@ If this is related to another GitHub issue, please link here too. A clear and concise description of any alternative solutions or features you've considered, if any. +### Build Info + +Describe the versions where you are observing the missing feature in: + + +- Isaac Lab Version: [e.g. 2.3.2] +- Isaac Sim Version: [e.g. 5.1, this can be obtained by `cat ${ISAACSIM_PATH}/VERSION`] + ### Additional context Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index b6e18b7fd54f..6e0582f37379 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -10,4 +10,12 @@ Basic questions, related to robot learning, that are not bugs or feature request Advanced/nontrivial questions, especially in areas where documentation is lacking, are very much welcome. -For questions that are related to running and understanding Isaac Sim, please post them at the official [Isaac Sim forums](https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/isaac_sim_forums.html). +For questions that are related to running and understanding Isaac Sim, please post them at the official [Isaac Sim forums](https://forums.developer.nvidia.com/c/omniverse/simulation/69). + +### Build Info + +Describe the versions that you are currently using: + + +- Isaac Lab Version: [e.g. 2.3.2] +- Isaac Sim Version: [e.g. 5.1, this can be obtained by `cat ${ISAACSIM_PATH}/VERSION`] diff --git a/.github/actions/combine-results/action.yml b/.github/actions/combine-results/action.yml index c5594316640a..8ed66e3b4603 100644 --- a/.github/actions/combine-results/action.yml +++ b/.github/actions/combine-results/action.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/actions/docker-build/action.yml b/.github/actions/docker-build/action.yml index 69a8db5ff0b6..2db402d42042 100644 --- a/.github/actions/docker-build/action.yml +++ b/.github/actions/docker-build/action.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 52e8d4a686ed..467122860141 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/labeler.yml b/.github/labeler.yml index c6869bb3c4c1..2e6837fdb71a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -26,7 +26,6 @@ infrastructure: - setup.py - pyproject.toml - .pre-commit-config.yaml - - .flake8 - isaaclab.sh - isaaclab.bat - docs/licenses/** diff --git a/.github/stale.yml b/.github/stale.yml index 788e21de1d0e..6205170b38bd 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/workflows/build.yml b/.github/workflows/build.yml index 8e648f109ea4..cbaa8f7b8e99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 000000000000..a5f91e934032 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,120 @@ +# 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 + +name: Check Documentation Links + +on: + # Run on pull requests that modify documentation + pull_request: + paths: + - 'docs/**' + - '**.md' + - '.github/workflows/check-links.yml' + # Run on pushes to main branches + push: + branches: + - main + - devel + - 'release/**' + paths: + - 'docs/**' + - '**.md' + - '.github/workflows/check-links.yml' + # Allow manual trigger + workflow_dispatch: + # Run weekly to catch external links that break over time + schedule: + - cron: '0 0 * * 0' # Every Sunday at midnight UTC + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-links: + name: Check for Broken Links + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + - name: Run Link Checker + uses: lycheeverse/lychee-action@v2 + with: + # Check all markdown files and documentation + args: >- + --verbose + --no-progress + --cache + --max-cache-age 1d + --exclude-path './docs/_build' + --exclude-path './apps/warp-*' + --exclude-path './logs' + --exclude-path './outputs' + --exclude-loopback + --exclude '^file://' + --exclude '^mailto:' + --exclude 'localhost' + --exclude '127\.0\.0\.1' + --exclude 'example\.com' + --exclude 'your-organization' + --exclude 'YOUR_' + --exclude 'yourdomain' + --exclude 'user@' + --exclude 'helm\.ngc\.nvidia\.com' + --exclude 'slurm\.schedmd\.com' + --max-retries 3 + --retry-wait-time 5 + --timeout 30 + --accept 200,201,202,203,204,206,301,302,303,307,308,429 + --scheme https + --scheme http + '*.md' + '**/*.md' + 'docs/**/*.rst' + 'docs/**/*.html' + # Output results to a file + output: ./lychee-output.md + # Fail action on broken links + fail: true + # Optional: Use GitHub token for authenticated requests (higher rate limit) + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Print results to logs + if: always() + run: | + echo "========================================" + echo "Link Checker Results:" + echo "========================================" + if [ -f ./lychee-output.md ]; then + cat ./lychee-output.md + echo "" + echo "========================================" + + # Also add to GitHub step summary for easy viewing + echo "## Link Checker Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + cat ./lychee-output.md >> $GITHUB_STEP_SUMMARY + else + echo "No output file generated" + echo "========================================" + fi + + - name: Fail job if broken links found + if: failure() + run: | + echo "❌ Broken links were found in the documentation!" + echo "Please review the link checker report above and fix all broken links." + exit 1 diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml deleted file mode 100644 index 86912ba5634e..000000000000 --- a/.github/workflows/cherry-pick.yml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -name: Cherry-pick from main to devel - -on: - push: - branches: - - main - -jobs: - cherry-pick: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 # fetch full history to access other branches - - - name: Set up Git config - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Get latest commit SHA on main - id: get_commit - run: echo "::set-output name=sha::$(git rev-parse HEAD)" - - - name: Checkout devel branch - run: git checkout devel - - - name: Cherry-pick latest commit from main - run: | - git cherry-pick ${{ steps.get_commit.outputs.sha }} - - - name: Push changes to devel - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git push origin devel diff --git a/.github/workflows/daily-compatibility.yml b/.github/workflows/daily-compatibility.yml index c0e95f7e1d77..bbf59e45160d 100644 --- a/.github/workflows/daily-compatibility.yml +++ b/.github/workflows/daily-compatibility.yml @@ -1,14 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 -name: Daily Backwards Compatibility Tests +name: Backwards Compatibility Tests on: schedule: # Run daily at 8 PM PST (4 AM UTC) - cron: '0 4 * * *' + workflow_dispatch: inputs: isaacsim_version: @@ -19,7 +20,7 @@ on: # Concurrency control to prevent parallel runs concurrency: - group: daily-compatibilit y-${{ github.ref }} + group: compatibility-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true permissions: @@ -29,20 +30,43 @@ permissions: env: NGC_API_KEY: ${{ secrets.NGC_API_KEY }} ISAACSIM_BASE_IMAGE: ${{ vars.ISAACSIM_BASE_IMAGE || 'nvcr.io/nvidia/isaac-sim' }} - ISAACSIM_DEFAULT_VERSION: '4.5.0' - DOCKER_IMAGE_TAG: isaac-lab-compat:${{ github.ref_name }}-${{ github.sha }} jobs: + setup-versions: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.set-versions.outputs.versions }} + steps: + - name: Set Isaac Sim Versions + id: set-versions + run: | + # Define all versions to test in one place + DEFAULT_VERSIONS='["4.5.0", "5.0.0"]' + + if [ -n "${{ github.event.inputs.isaacsim_version }}" ]; then + # If a specific version is provided via workflow_dispatch, use only that + echo "versions=[\"${{ github.event.inputs.isaacsim_version }}\"]" >> $GITHUB_OUTPUT + else + # Otherwise, use all default versions + echo "versions=$DEFAULT_VERSIONS" >> $GITHUB_OUTPUT + fi + test-isaaclab-tasks-compat: + needs: setup-versions runs-on: [self-hosted, gpu] timeout-minutes: 180 continue-on-error: true + strategy: + matrix: + isaacsim_version: ${{ fromJson(needs.setup-versions.outputs.versions) }} + fail-fast: false env: CUDA_VISIBLE_DEVICES: all NVIDIA_VISIBLE_DEVICES: all NVIDIA_DRIVER_CAPABILITIES: all CUDA_HOME: /usr/local/cuda LD_LIBRARY_PATH: /usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64 + DOCKER_IMAGE_TAG: isaac-lab-compat:${{ github.ref_name }}-${{ github.sha }}-${{ matrix.isaacsim_version }} steps: - name: Checkout Code @@ -56,7 +80,7 @@ jobs: with: image-tag: ${{ env.DOCKER_IMAGE_TAG }} isaacsim-base-image: ${{ env.ISAACSIM_BASE_IMAGE }} - isaacsim-version: ${{ github.event.inputs.isaacsim_version || env.ISAACSIM_DEFAULT_VERSION }} + isaacsim-version: ${{ matrix.isaacsim_version }} - name: Run IsaacLab Tasks Tests uses: ./.github/actions/run-tests @@ -80,20 +104,26 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: isaaclab-tasks-compat-results + name: isaaclab-tasks-compat-results-${{ matrix.isaacsim_version }} path: reports/isaaclab-tasks-compat-report.xml retention-days: 7 compression-level: 9 test-general-compat: + needs: setup-versions runs-on: [self-hosted, gpu] timeout-minutes: 180 + strategy: + matrix: + isaacsim_version: ${{ fromJson(needs.setup-versions.outputs.versions) }} + fail-fast: false env: CUDA_VISIBLE_DEVICES: all NVIDIA_VISIBLE_DEVICES: all NVIDIA_DRIVER_CAPABILITIES: all CUDA_HOME: /usr/local/cuda LD_LIBRARY_PATH: /usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64 + DOCKER_IMAGE_TAG: isaac-lab-compat:${{ github.ref_name }}-${{ github.sha }}-${{ matrix.isaacsim_version }} steps: - name: Checkout Code @@ -107,7 +137,7 @@ jobs: with: image-tag: ${{ env.DOCKER_IMAGE_TAG }} isaacsim-base-image: ${{ env.ISAACSIM_BASE_IMAGE }} - isaacsim-version: ${{ github.event.inputs.isaacsim_version || env.ISAACSIM_DEFAULT_VERSION }} + isaacsim-version: ${{ matrix.isaacsim_version }} - name: Run General Tests uses: ./.github/actions/run-tests @@ -131,7 +161,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: general-tests-compat-results + name: general-tests-compat-results-${{ matrix.isaacsim_version }} path: reports/general-tests-compat-report.xml retention-days: 7 compression-level: 9 @@ -152,18 +182,12 @@ jobs: run: | mkdir -p reports - - name: Download Test Results - uses: actions/download-artifact@v4 - with: - name: isaaclab-tasks-compat-results - path: reports/ - continue-on-error: true - - - name: Download General Test Results + - name: Download All Test Results uses: actions/download-artifact@v4 with: - name: general-tests-compat-results + pattern: '*-compat-results-*' path: reports/ + merge-multiple: true continue-on-error: true - name: Combine All Test Results @@ -185,14 +209,14 @@ jobs: uses: dorny/test-reporter@v1 if: always() with: - name: IsaacLab Compatibility Test Results + name: IsaacLab Compatibility Test Results (${{ github.event_name }}) path: reports/combined-compat-results.xml reporter: java-junit max-annotations: '50' - report-title: "IsaacLab Compatibility Test Results - ${{ github.workflow }}" + report-title: "IsaacLab Compatibility Test Results - ${{ github.event_name }} - ${{ github.ref_name }}" notify-compatibility-status: - needs: [combine-compat-results] + needs: [setup-versions, combine-compat-results] runs-on: [self-hosted, gpu] if: always() @@ -205,10 +229,12 @@ jobs: - name: Create Compatibility Report run: | - ISAACSIM_VERSION_USED="${{ github.event.inputs.isaacsim_version || env.ISAACSIM_DEFAULT_VERSION }}" + TRIGGER_INFO="**Trigger:** ${{ github.event_name }}" + ISAACSIM_VERSIONS="${{ join(fromJson(needs.setup-versions.outputs.versions), ', ') }}" echo "## Daily Backwards Compatibility Test Results" > compatibility-report.md echo "" >> compatibility-report.md - echo "**IsaacSim Version:** $ISAACSIM_VERSION_USED" >> compatibility-report.md + echo "$TRIGGER_INFO" >> compatibility-report.md + echo "**IsaacSim Versions Tested:** $ISAACSIM_VERSIONS" >> compatibility-report.md echo "**Branch:** ${{ github.ref_name }}" >> compatibility-report.md echo "**Commit:** ${{ github.sha }}" >> compatibility-report.md echo "**Run ID:** ${{ github.run_id }}" >> compatibility-report.md diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 08bf3d2a8bf1..9af2d6e94f0b 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 8b06dc14407b..593aec9a2cb0 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/workflows/license-check.yaml b/.github/workflows/license-check.yaml index 6260199e1dc0..e3753ffcb6b5 100644 --- a/.github/workflows/license-check.yaml +++ b/.github/workflows/license-check.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -27,6 +27,12 @@ jobs: - name: Clean up disk space run: | rm -rf /opt/hostedtoolcache + rm -rf /usr/share/dotnet + rm -rf /opt/ghc + docker container prune -f + docker image prune -af + docker volume prune -f || true + - name: Set up Python uses: actions/setup-python@v4 diff --git a/.github/workflows/license-exceptions.json b/.github/workflows/license-exceptions.json index 4e35db15646b..27b3b9c65522 100644 --- a/.github/workflows/license-exceptions.json +++ b/.github/workflows/license-exceptions.json @@ -7,6 +7,10 @@ "package": "isaaclab_assets", "license": null }, + { + "package": "isaaclab_contrib", + "license": null + }, { "package": "isaaclab_mimic", "license": null diff --git a/.github/workflows/mirror-repository.yml b/.github/workflows/mirror-repository.yml deleted file mode 100644 index 8f00505ef0df..000000000000 --- a/.github/workflows/mirror-repository.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -name: Mirror Repository - -on: - push: - branches: - - main - -# Concurrency control to prevent parallel runs -concurrency: - group: mirror-repository-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - -jobs: - mirror-repository: - runs-on: ubuntu-latest - timeout-minutes: 30 - # Only run on specific repository - if: github.repository == 'isaac-sim/IsaacLab' - environment: - name: mirror-production - url: https://github.com/${{ vars.TARGET_REPO }} - - steps: - - name: Install Git LFS - run: | - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash - sudo apt-get install git-lfs - git lfs install - - - name: Checkout Code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - lfs: true - - - name: Configure Git - run: | - git config --global user.name "Isaac LAB CI Bot" - git config --global user.email "isaac-lab-ci-bot@nvidia.com" - - - name: Set Target Repository URL - run: | - echo "TARGET_REPO=${{ vars.TARGET_REPO }}" >> $GITHUB_ENV - echo "TARGET_BRANCH=${{ vars.TARGET_BRANCH || 'main' }}" >> $GITHUB_ENV - - - name: Mirror to Target Repository - run: | - # Remove existing target remote if it exists - git remote remove target 2>/dev/null || true - - # Add target remote with authentication - git remote add target https://${{ secrets.GH_TOKEN }}@github.com/${TARGET_REPO}.git - - # Fetch latest from target to avoid conflicts - git fetch target $TARGET_BRANCH 2>/dev/null || true - - # Push to target repository and branch (source is always main) - git push --force target main:$TARGET_BRANCH - - echo "✅ Successfully mirrored main to ${TARGET_REPO}:$TARGET_BRANCH" diff --git a/.github/workflows/postmerge-ci.yml b/.github/workflows/postmerge-ci.yml index b5b05795353d..4a1f34d38a16 100644 --- a/.github/workflows/postmerge-ci.yml +++ b/.github/workflows/postmerge-ci.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 05e0d7d60aff..f71f1f373899 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,4 +15,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 + with: + python-version: "3.12" - uses: pre-commit/action@v3.0.0 diff --git a/.gitignore b/.gitignore index 08d2e8dee5a9..7afb58e9ee00 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,7 @@ tests/ # Docker history .isaac-lab-docker-history + +# TacSL sensor +**/tactile_record/* +**/gelsight_r15_data/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86e513bc0a25..426a84b59b79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,19 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 repos: - - repo: https://github.com/python/black - rev: 24.3.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.10 hooks: - - id: black - args: ["--line-length", "120", "--unstable"] - - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - additional_dependencies: [flake8-simplify, flake8-return] + # Run the linter + - id: ruff + args: ["--fix"] + # Run the formatter + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-symlinks @@ -31,21 +29,8 @@ repos: - id: check-shebang-scripts-are-executable - id: detect-private-key - id: debug-statements - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - name: isort (python) - args: ["--profile", "black", "--filter-files"] - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 - hooks: - - id: pyupgrade - args: ["--py310-plus"] - # FIXME: This is a hack because Pytorch does not like: torch.Tensor | dict aliasing - exclude: "source/isaaclab/isaaclab/envs/common.py|source/isaaclab/isaaclab/ui/widgets/image_plot.py|source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_loader.py" - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.4.1 hooks: - id: codespell additional_dependencies: @@ -57,7 +42,7 @@ repos: # hooks: # - id: pyright - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.1 + rev: v1.5.5 hooks: - id: insert-license files: \.(py|ya?ml)$ @@ -69,7 +54,7 @@ repos: exclude: "source/isaaclab_mimic/|scripts/imitation_learning/isaaclab_mimic/" # Apache 2.0 license for mimic files - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.1 + rev: v1.5.5 hooks: - id: insert-license files: ^(source/isaaclab_mimic|scripts/imitation_learning/isaaclab_mimic)/.*\.py$ diff --git a/.vscode/tools/settings.template.json b/.vscode/tools/settings.template.json index fa0f1c823169..4b07a6a8f9ad 100644 --- a/.vscode/tools/settings.template.json +++ b/.vscode/tools/settings.template.json @@ -61,15 +61,8 @@ ], // This enables python language server. Seems to work slightly better than jedi: "python.languageServer": "Pylance", - // We use "black" as a formatter: - "python.formatting.provider": "black", - "python.formatting.blackArgs": ["--line-length", "120"], - // Use flake8 for linting - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "python.linting.flake8Args": [ - "--max-line-length=120" - ], + // Use ruff as a formatter and linter + "ruff.configuration": "${workspaceFolder}/pyproject.toml", // Use docstring generator "autoDocstring.docstringFormat": "google", "autoDocstring.guessTypes": true, diff --git a/.vscode/tools/setup_vscode.py b/.vscode/tools/setup_vscode.py index 6cf12b613f89..9e2c6375ab75 100644 --- a/.vscode/tools/setup_vscode.py +++ b/.vscode/tools/setup_vscode.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/CITATION.cff b/CITATION.cff index 71b49b901a31..70d123b55242 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,53 +1,227 @@ cff-version: 1.2.0 -message: "If you use this software, please cite both the Isaac Lab repository and the Orbit paper." +message: "If you use this software, please cite the technical report of Isaac Lab." title: Isaac Lab -version: 2.3.0 -repository-code: https://github.com/NVIDIA-Omniverse/IsaacLab +version: 2.3.2 +repository-code: https://github.com/isaac-sim/IsaacLab type: software authors: - name: Isaac Lab Project Developers identifiers: - type: url - value: https://github.com/NVIDIA-Omniverse/IsaacLab -url: https://isaac-sim.github.io/IsaacLab/main/index.html + value: https://github.com/isaac-sim/IsaacLab + - type: doi + value: 10.48550/arXiv.2511.04831 +url: https://isaac-sim.github.io/IsaacLab license: BSD-3-Clause preferred-citation: type: article - title: Orbit - A Unified Simulation Framework for Interactive Robot Learning Environments + title: Isaac Lab - A GPU-Accelerated Simulation Framework for Multi-Modal Robot Learning authors: - family-names: Mittal given-names: Mayank - - family-names: Yu - given-names: Calvin - - family-names: Yu - given-names: Qinxi - - family-names: Liu - given-names: Jingzhou + - family-names: Roth + given-names: Pascal + - family-names: Tigue + given-names: James + - family-names: Richard + given-names: Antoine + - family-names: Zhang + given-names: Octi + - family-names: Du + given-names: Peter + - family-names: Serrano-Muñoz + given-names: Antonio + - family-names: Yao + given-names: Xinjie + - family-names: Zurbrügg + given-names: René - family-names: Rudin given-names: Nikita - - family-names: Hoeller - given-names: David - - family-names: Yuan - given-names: Jia Lin - - family-names: Singh - given-names: Ritvik + - family-names: Wawrzyniak + given-names: Lukasz + - family-names: Rakhsha + given-names: Milad + - family-names: Denzler + given-names: Alain + - family-names: Heiden + given-names: Eric + - family-names: Borovicka + given-names: Ales + - family-names: Ahmed + given-names: Ossama + - family-names: Akinola + given-names: Iretiayo + - family-names: Anwar + given-names: Abrar + - family-names: Carlson + given-names: Mark T. + - family-names: Feng + given-names: Ji Yuan + - family-names: Garg + given-names: Animesh + - family-names: Gasoto + given-names: Renato + - family-names: Gulich + given-names: Lionel - family-names: Guo - given-names: Yunrong + given-names: Yijie + - family-names: Gussert + given-names: M. + - family-names: Hansen + given-names: Alex + - family-names: Kulkarni + given-names: Mihir + - family-names: Li + given-names: Chenran + - family-names: Liu + given-names: Wei + - family-names: Makoviychuk + given-names: Viktor + - family-names: Malczyk + given-names: Grzegorz - family-names: Mazhar given-names: Hammad + - family-names: Moghani + given-names: Masoud + - family-names: Murali + given-names: Adithyavairavan + - family-names: Noseworthy + given-names: Michael + - family-names: Poddubny + given-names: Alexander + - family-names: Ratliff + given-names: Nathan + - family-names: Rehberg + given-names: Welf + - family-names: Schwarke + given-names: Clemens + - family-names: Singh + given-names: Ritvik + - family-names: Smith + given-names: James Latham + - family-names: Tang + given-names: Bingjie + - family-names: Thaker + given-names: Ruchik + - family-names: Trepte + given-names: Matthew + - family-names: Van Wyk + given-names: Karl + - family-names: Yu + given-names: Fangzhou + - family-names: Millane + given-names: Alex + - family-names: Ramasamy + given-names: Vikram + - family-names: Steiner + given-names: Remo + - family-names: Subramanian + given-names: Sangeeta + - family-names: Volk + given-names: Clemens + - family-names: Chen + given-names: CY + - family-names: Jawale + given-names: Neel + - family-names: Kuruttukulam + given-names: Ashwin Varghese + - family-names: Lin + given-names: Michael A. - family-names: Mandlekar given-names: Ajay - - family-names: Babich - given-names: Buck - - family-names: State - given-names: Gavriel + - family-names: Patzwaldt + given-names: Karsten + - family-names: Welsh + given-names: John + - family-names: Lafleche + given-names: Jean-Francois + - family-names: Moënne-Loccoz + given-names: Nicolas + - family-names: Park + given-names: Soowan + - family-names: Stepinski + given-names: Rob + - family-names: Van Gelder + given-names: Dirk + - family-names: Amevor + given-names: Chris + - family-names: Carius + given-names: Jan + - family-names: Chang + given-names: Jumyung + - family-names: He Chen + given-names: Anka + - family-names: Ciechomski + given-names: Pablo de Heras + - family-names: Daviet + given-names: Gilles + - family-names: Mohajerani + given-names: Mohammad + - family-names: von Muralt + given-names: Julia + - family-names: Reutskyy + given-names: Viktor + - family-names: Sauter + given-names: Michael + - family-names: Schirm + given-names: Simon + - family-names: Shi + given-names: Eric L. + - family-names: Terdiman + given-names: Pierre + - family-names: Vilella + given-names: Kenny + - family-names: Widmer + given-names: Tobias + - family-names: Yeoman + given-names: Gordon + - family-names: Chen + given-names: Tiffany + - family-names: Grizan + given-names: Sergey + - family-names: Li + given-names: Cathy + - family-names: Li + given-names: Lotus + - family-names: Smith + given-names: Connor + - family-names: Wiltz + given-names: Rafael + - family-names: Alexis + given-names: Kostas + - family-names: Chang + given-names: Yan + - family-names: Fan + given-names: Linxi "Jim" + - family-names: Farshidian + given-names: Farbod + - family-names: Handa + given-names: Ankur + - family-names: Huang + given-names: Spencer - family-names: Hutter given-names: Marco - - family-names: Garg - given-names: Animesh - journal: IEEE Robotics and Automation Letters - volume: 8 - issue: 6 - pages: 3740-3747 - year: 2023 - doi: 10.1109/LRA.2023.3270034 + - family-names: Narang + given-names: Yashraj + - family-names: Pouya + given-names: Soha + - family-names: Sheng + given-names: Shiwei + - family-names: Zhu + given-names: Yuke + - family-names: Macklin + given-names: Miles + - family-names: Moravanszky + given-names: Adam + - family-names: Reist + given-names: Philipp + - family-names: Guo + given-names: Yunrong + - family-names: Hoeller + given-names: David + - family-names: State + given-names: Gavriel + journal: arXiv preprint arXiv:2511.04831 + year: 2025 + url: https://arxiv.org/abs/2511.04831 + doi: 10.48550/arXiv.2511.04831 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ebfc174f0a70..9fbfe7f1bf30 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,6 +21,7 @@ Guidelines for modifications: * Antonio Serrano-Muñoz * Ben Johnston +* Brian McCann * Clemens Schwarke * David Hoeller * Farbod Farshidian @@ -35,6 +36,7 @@ Guidelines for modifications: * Pascal Roth * Sheikh Dawood * Ossama Ahmed +* Greg Attra ## Contributors @@ -51,6 +53,7 @@ Guidelines for modifications: * Bingjie Tang * Brayden Zhang * Brian Bingham +* Brian McCann * Cameron Upright * Calvin Yu * Cathy Y. Li @@ -60,18 +63,22 @@ Guidelines for modifications: * CY (Chien-Ying) Chen * David Yang * Dhananjay Shendre +* Dongxuan Fan * Dorsa Rohani * Emily Sturman +* Emmanuel Ferdman * Fabian Jenelten * Felipe Mohr * Felix Yu * Gary Lvov * Giulio Romualdi +* Grzegorz Malczyk * Haoran Zhou * Harsh Patel * HoJin Jeon * Hongwei Xiong * Hongyu Li +* Hougant Chen * Huihua Zhao * Iretiayo Akinola * Jack Zeng @@ -83,9 +90,12 @@ Guidelines for modifications: * Jinghuan Shang * Jingzhou Liu * Jinqi Wei +* Jinyeob Kim * Johnson Sun +* Juana Du * Kaixi Bao * Kris Wilson +* Krishna Lakhi * Kourosh Darvish * Kousheek Chakraborty * Lionel Gulich @@ -95,11 +105,14 @@ Guidelines for modifications: * Lukas Fröhlich * Manuel Schweiger * Masoud Moghani +* Mateo Guaman Castro * Maurice Rahme * Michael Gussert * Michael Noseworthy * Michael Lin * Miguel Alonso Jr +* Mihir Kulkarni +* Mingxue Gu * Mingyu Lee * Muhong Guo * Narendra Dahile @@ -123,9 +136,12 @@ Guidelines for modifications: * René Zurbrügg * Ritvik Singh * Rosario Scalise +* Ryan Gresia * Ryley McCarroll +* Sahara Yuta * Sergey Grizan * Shafeef Omar +* Shane Reetz * Shaoshu Su * Shaurya Dewan * Sixiang Chen @@ -133,12 +149,15 @@ Guidelines for modifications: * Stefan Van de Mosselaer * Stephan Pleines * Tiffany Chen +* Trushant Adeshara * Tyler Lum * Victor Khaustov * Virgilio Gómez Lambo * Vladimir Fokow * Wei Yang +* Welf Rehberg * Xavier Nal +* Xiaodi Yuan * Xinjie Yao * Xinpeng Liu * Yang Jin @@ -148,10 +167,14 @@ Guidelines for modifications: * Yujian Zhang * Yun Liu * Zehao Wang +* Zijian Li * Ziqi Fan * Zoe McCarthy * David Leon * Song Yi +* Weihua Zhang +* Tsz Ki GAO +* Anke Zhao ## Acknowledgements diff --git a/README.md b/README.md index edbf2dfdb548..ca5dc349d67c 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,13 @@ cameras, LIDAR, or contact sensors. The framework's GPU acceleration enables use computations faster, which is key for iterative processes like reinforcement learning and data-intensive tasks. Moreover, Isaac Lab can run locally or be distributed across the cloud, offering flexibility for large-scale deployments. +A detailed description of Isaac Lab can be found in our [arXiv paper](https://arxiv.org/abs/2511.04831). ## Key Features Isaac Lab offers a comprehensive set of tools and environments designed to facilitate robot learning: -- **Robots**: A diverse collection of robots, from manipulators, quadrupeds, to humanoids, with 16 commonly available models. +- **Robots**: A diverse collection of robots, from manipulators, quadrupeds, to humanoids, with more than 16 commonly available models. - **Environments**: Ready-to-train implementations of more than 30 environments, which can be trained with popular reinforcement learning frameworks such as RSL RL, SKRL, RL Games, or Stable Baselines. We also support multi-agent reinforcement learning. - **Physics**: Rigid bodies, articulated systems, deformable objects - **Sensors**: RGB/depth/segmentation cameras, camera annotations, IMU, contact sensors, ray casters. @@ -56,7 +57,7 @@ dependency versions for Isaac Sim. | Isaac Lab Version | Isaac Sim Version | | ----------------------------- | ------------------------- | -| `main` branch | Isaac Sim 4.5 / 5.0 | +| `main` branch | Isaac Sim 4.5 / 5.0 / 5.1 | | `v2.3.X` | Isaac Sim 4.5 / 5.0 / 5.1 | | `v2.2.X` | Isaac Sim 4.5 / 5.0 | | `v2.1.X` | Isaac Sim 4.5 | @@ -87,7 +88,7 @@ innovation in robotics and simulation. Please see the [troubleshooting](https://isaac-sim.github.io/IsaacLab/main/source/refs/troubleshooting.html) section for common fixes or [submit an issue](https://github.com/isaac-sim/IsaacLab/issues). -For issues related to Isaac Sim, we recommend checking its [documentation](https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html) +For issues related to Isaac Sim, we recommend checking its [documentation](https://docs.isaacsim.omniverse.nvidia.com/latest/index.html) or opening a question on its [forums](https://forums.developer.nvidia.com/c/agx-autonomous-machines/isaac/67). ## Support @@ -119,20 +120,22 @@ Note that Isaac Lab requires Isaac Sim, which includes components under propriet Note that the `isaaclab_mimic` extension requires cuRobo, which has proprietary licensing terms that can be found in [`docs/licenses/dependencies/cuRobo-license.txt`](docs/licenses/dependencies/cuRobo-license.txt). -## Acknowledgement -Isaac Lab development initiated from the [Orbit](https://isaac-orbit.github.io/) framework. We would appreciate if -you would cite it in academic publications as well: +## Citation + +If you use Isaac Lab in your research, please cite the technical report: ``` -@article{mittal2023orbit, - author={Mittal, Mayank and Yu, Calvin and Yu, Qinxi and Liu, Jingzhou and Rudin, Nikita and Hoeller, David and Yuan, Jia Lin and Singh, Ritvik and Guo, Yunrong and Mazhar, Hammad and Mandlekar, Ajay and Babich, Buck and State, Gavriel and Hutter, Marco and Garg, Animesh}, - journal={IEEE Robotics and Automation Letters}, - title={Orbit: A Unified Simulation Framework for Interactive Robot Learning Environments}, - year={2023}, - volume={8}, - number={6}, - pages={3740-3747}, - doi={10.1109/LRA.2023.3270034} +@article{mittal2025isaaclab, + title={Isaac Lab: A GPU-Accelerated Simulation Framework for Multi-Modal Robot Learning}, + author={Mayank Mittal and Pascal Roth and James Tigue and Antoine Richard and Octi Zhang and Peter Du and Antonio Serrano-Muñoz and Xinjie Yao and René Zurbrügg and Nikita Rudin and Lukasz Wawrzyniak and Milad Rakhsha and Alain Denzler and Eric Heiden and Ales Borovicka and Ossama Ahmed and Iretiayo Akinola and Abrar Anwar and Mark T. Carlson and Ji Yuan Feng and Animesh Garg and Renato Gasoto and Lionel Gulich and Yijie Guo and M. Gussert and Alex Hansen and Mihir Kulkarni and Chenran Li and Wei Liu and Viktor Makoviychuk and Grzegorz Malczyk and Hammad Mazhar and Masoud Moghani and Adithyavairavan Murali and Michael Noseworthy and Alexander Poddubny and Nathan Ratliff and Welf Rehberg and Clemens Schwarke and Ritvik Singh and James Latham Smith and Bingjie Tang and Ruchik Thaker and Matthew Trepte and Karl Van Wyk and Fangzhou Yu and Alex Millane and Vikram Ramasamy and Remo Steiner and Sangeeta Subramanian and Clemens Volk and CY Chen and Neel Jawale and Ashwin Varghese Kuruttukulam and Michael A. Lin and Ajay Mandlekar and Karsten Patzwaldt and John Welsh and Huihua Zhao and Fatima Anes and Jean-Francois Lafleche and Nicolas Moënne-Loccoz and Soowan Park and Rob Stepinski and Dirk Van Gelder and Chris Amevor and Jan Carius and Jumyung Chang and Anka He Chen and Pablo de Heras Ciechomski and Gilles Daviet and Mohammad Mohajerani and Julia von Muralt and Viktor Reutskyy and Michael Sauter and Simon Schirm and Eric L. Shi and Pierre Terdiman and Kenny Vilella and Tobias Widmer and Gordon Yeoman and Tiffany Chen and Sergey Grizan and Cathy Li and Lotus Li and Connor Smith and Rafael Wiltz and Kostas Alexis and Yan Chang and David Chu and Linxi "Jim" Fan and Farbod Farshidian and Ankur Handa and Spencer Huang and Marco Hutter and Yashraj Narang and Soha Pouya and Shiwei Sheng and Yuke Zhu and Miles Macklin and Adam Moravanszky and Philipp Reist and Yunrong Guo and David Hoeller and Gavriel State}, + journal={arXiv preprint arXiv:2511.04831}, + year={2025}, + url={https://arxiv.org/abs/2511.04831} } ``` + +## Acknowledgement + +Isaac Lab development initiated from the [Orbit](https://isaac-orbit.github.io/) framework. +We gratefully acknowledge the authors of Orbit for their foundational contributions. diff --git a/VERSION b/VERSION index 276cbf9e2858..f90b1afc082f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.3.0 +2.3.2 diff --git a/apps/isaaclab.python.headless.kit b/apps/isaaclab.python.headless.kit index 5e93d229c043..03a6bf196d01 100644 --- a/apps/isaaclab.python.headless.kit +++ b/apps/isaaclab.python.headless.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python Headless" description = "An app for running Isaac Lab headlessly" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "headless"] diff --git a/apps/isaaclab.python.headless.rendering.kit b/apps/isaaclab.python.headless.rendering.kit index b37f33999bf4..870c65bb4bfd 100644 --- a/apps/isaaclab.python.headless.rendering.kit +++ b/apps/isaaclab.python.headless.rendering.kit @@ -9,7 +9,7 @@ [package] title = "Isaac Lab Python Headless Camera" description = "An app for running Isaac Lab headlessly with rendering enabled" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "camera", "minimal"] diff --git a/apps/isaaclab.python.kit b/apps/isaaclab.python.kit index 04c996aa98ff..2435019c89ca 100644 --- a/apps/isaaclab.python.kit +++ b/apps/isaaclab.python.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python" description = "An app for running Isaac Lab" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "usd"] @@ -38,7 +38,7 @@ keywords = ["experience", "app", "usd"] # Isaac Sim Extra "isaacsim.asset.importer.mjcf" = {} -"isaacsim.asset.importer.urdf" = {} +"isaacsim.asset.importer.urdf" = {version = "2.4.31", exact = true} "omni.physx.bundle" = {} "omni.physx.tensors" = {} "omni.replicator.core" = {} diff --git a/apps/isaaclab.python.rendering.kit b/apps/isaaclab.python.rendering.kit index 73c181a0d685..d27498bd4054 100644 --- a/apps/isaaclab.python.rendering.kit +++ b/apps/isaaclab.python.rendering.kit @@ -9,7 +9,7 @@ [package] title = "Isaac Lab Python Camera" description = "An app for running Isaac Lab with rendering enabled" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "camera", "minimal"] diff --git a/apps/isaaclab.python.xr.openxr.headless.kit b/apps/isaaclab.python.xr.openxr.headless.kit index 4fa2bfc09850..57b78c1b2907 100644 --- a/apps/isaaclab.python.xr.openxr.headless.kit +++ b/apps/isaaclab.python.xr.openxr.headless.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python OpenXR Headless" description = "An app for running Isaac Lab with OpenXR in headless mode" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "usd", "headless"] diff --git a/apps/isaaclab.python.xr.openxr.kit b/apps/isaaclab.python.xr.openxr.kit index 4150eae64494..8fe9980e50b6 100644 --- a/apps/isaaclab.python.xr.openxr.kit +++ b/apps/isaaclab.python.xr.openxr.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python OpenXR" description = "An app for running Isaac Lab with OpenXR" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "usd"] diff --git a/apps/isaacsim_4_5/isaaclab.python.headless.kit b/apps/isaacsim_4_5/isaaclab.python.headless.kit index 944e284c4521..0fb9eaeffff3 100644 --- a/apps/isaacsim_4_5/isaaclab.python.headless.kit +++ b/apps/isaacsim_4_5/isaaclab.python.headless.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python Headless" description = "An app for running Isaac Lab headlessly" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "headless"] diff --git a/apps/isaacsim_4_5/isaaclab.python.headless.rendering.kit b/apps/isaacsim_4_5/isaaclab.python.headless.rendering.kit index cb1b4e8a25de..10b3efc84bfc 100644 --- a/apps/isaacsim_4_5/isaaclab.python.headless.rendering.kit +++ b/apps/isaacsim_4_5/isaaclab.python.headless.rendering.kit @@ -9,7 +9,7 @@ [package] title = "Isaac Lab Python Headless Camera" description = "An app for running Isaac Lab headlessly with rendering enabled" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "camera", "minimal"] diff --git a/apps/isaacsim_4_5/isaaclab.python.kit b/apps/isaacsim_4_5/isaaclab.python.kit index 89db9ffb0d6e..5fc9b1effd9d 100644 --- a/apps/isaacsim_4_5/isaaclab.python.kit +++ b/apps/isaacsim_4_5/isaaclab.python.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python" description = "An app for running Isaac Lab" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "usd"] diff --git a/apps/isaacsim_4_5/isaaclab.python.rendering.kit b/apps/isaacsim_4_5/isaaclab.python.rendering.kit index df2ee90bf166..ad234141fd9b 100644 --- a/apps/isaacsim_4_5/isaaclab.python.rendering.kit +++ b/apps/isaacsim_4_5/isaaclab.python.rendering.kit @@ -9,7 +9,7 @@ [package] title = "Isaac Lab Python Camera" description = "An app for running Isaac Lab with rendering enabled" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "isaaclab", "python", "camera", "minimal"] diff --git a/apps/isaacsim_4_5/isaaclab.python.xr.openxr.headless.kit b/apps/isaacsim_4_5/isaaclab.python.xr.openxr.headless.kit index 5839ae8acc31..82f7e5cd62ab 100644 --- a/apps/isaacsim_4_5/isaaclab.python.xr.openxr.headless.kit +++ b/apps/isaacsim_4_5/isaaclab.python.xr.openxr.headless.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python OpenXR Headless" description = "An app for running Isaac Lab with OpenXR in headless mode" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "usd", "headless"] diff --git a/apps/isaacsim_4_5/isaaclab.python.xr.openxr.kit b/apps/isaacsim_4_5/isaaclab.python.xr.openxr.kit index 24f4663c2e05..d27e5c444c21 100644 --- a/apps/isaacsim_4_5/isaaclab.python.xr.openxr.kit +++ b/apps/isaacsim_4_5/isaaclab.python.xr.openxr.kit @@ -5,7 +5,7 @@ [package] title = "Isaac Lab Python OpenXR" description = "An app for running Isaac Lab with OpenXR" -version = "2.3.0" +version = "2.3.2" # That makes it browsable in UI with "experience" filter keywords = ["experience", "app", "usd"] diff --git a/docker/Dockerfile.curobo b/docker/Dockerfile.curobo index 8e7ea4baffba..0830adebf18b 100644 --- a/docker/Dockerfile.curobo +++ b/docker/Dockerfile.curobo @@ -68,7 +68,13 @@ RUN set -euo pipefail && \ wget -q https://developer.download.nvidia.com/compute/cuda/repos/${cuda_repo}/x86_64/cuda-${cuda_repo}.pin && \ mv cuda-${cuda_repo}.pin /etc/apt/preferences.d/cuda-repository-pin-600 && \ apt-get update && \ - apt-get install -y --no-install-recommends cuda-toolkit-12-8 && \ + apt-get install -y --no-install-recommends \ + cuda-toolkit-12-8 \ + libcudnn9-cuda-12 \ + libcusparselt0 \ + libnccl2 \ + libnccl-dev \ + libnvjitlink-12-8 && \ apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* @@ -115,18 +121,23 @@ RUN touch /bin/nvidia-smi && \ mkdir -p /var/run/nvidia-persistenced && \ touch /var/run/nvidia-persistenced/socket +# HACK: Remove pre-bundled torch BEFORE installing Isaac Lab dependencies. +# This forces isaaclab.sh --install to install torch fresh to site-packages, +# rather than skipping because it detects the pre-bundled version. +RUN rm -rf ${ISAACSIM_ROOT_PATH}/exts/omni.isaac.ml_archive/pip_prebundle/torch* + # installing Isaac Lab dependencies # use pip caching to avoid reinstalling large packages RUN --mount=type=cache,target=${DOCKER_USER_HOME}/.cache/pip \ ${ISAACLAB_PATH}/isaaclab.sh --install +# HACK: Uninstall quadprog as it causes issues with some reinforcement learning frameworks +RUN ${ISAACLAB_PATH}/isaaclab.sh -p -m pip uninstall -y quadprog + # Install cuRobo from source (pinned commit); needs CUDA env and Torch RUN ${ISAACLAB_PATH}/isaaclab.sh -p -m pip install --no-build-isolation \ "nvidia-curobo @ git+https://github.com/NVlabs/curobo.git@ebb71702f3f70e767f40fd8e050674af0288abe8" -# HACK: Remove install of quadprog dependency -RUN ${ISAACLAB_PATH}/isaaclab.sh -p -m pip uninstall -y quadprog - # aliasing isaaclab.sh and python for convenience RUN echo "export ISAACLAB_PATH=${ISAACLAB_PATH}" >> ${HOME}/.bashrc && \ echo "alias isaaclab=${ISAACLAB_PATH}/isaaclab.sh" >> ${HOME}/.bashrc && \ diff --git a/docker/container.py b/docker/container.py index 5b2f82163a11..ab92d816ffac 100755 --- a/docker/container.py +++ b/docker/container.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -57,9 +57,19 @@ def parse_cli_args() -> argparse.Namespace: " passed to suffix, then the produced docker image and container will be named ``isaac-lab-base-custom``." ), ) + parent_parser.add_argument( + "--info", + action="store_true", + help="Print the container interface information. This is useful for debugging purposes.", + ) # Actual command definition begins here subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser( + "build", + help="Build the docker image without creating the container.", + parents=[parent_parser], + ) subparsers.add_parser( "start", help="Build the docker image and create the container in detached mode.", @@ -107,9 +117,23 @@ def main(args: argparse.Namespace): envs=args.env_files, suffix=args.suffix, ) + if args.info: + print("[INFO] Printing container interface information...\n") + ci.print_info() + return print(f"[INFO] Using container profile: {ci.profile}") - if args.command == "start": + if args.command == "build": + # check if x11 forwarding is enabled + x11_outputs = x11_utils.x11_check(ci.statefile) + # if x11 forwarding is enabled, add the x11 yaml and environment variables + if x11_outputs is not None: + (x11_yaml, x11_envar) = x11_outputs + ci.add_yamls += x11_yaml + ci.environ.update(x11_envar) + # build the image + ci.build() + elif args.command == "start": # check if x11 forwarding is enabled x11_outputs = x11_utils.x11_check(ci.statefile) # if x11 forwarding is enabled, add the x11 yaml and environment variables diff --git a/docker/docker-compose.cloudxr-runtime.patch.yaml b/docker/docker-compose.cloudxr-runtime.patch.yaml index ed27fc44a9ba..5615aed29ac7 100644 --- a/docker/docker-compose.cloudxr-runtime.patch.yaml +++ b/docker/docker-compose.cloudxr-runtime.patch.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docker/docker-compose.yaml b/docker/docker-compose.yaml index d7809109bdc1..09fde19be7c2 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,10 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 # Here we set the parts that would -# be re-used between services to an +# be reused between services to an # extension field # https://docs.docker.com/compose/compose-file/compose-file-v3/#extension-fields x-default-isaac-lab-volumes: &default-isaac-lab-volumes diff --git a/docker/test/test_docker.py b/docker/test/test_docker.py index cce0e0a137bb..85fd66348f85 100644 --- a/docker/test/test_docker.py +++ b/docker/test/test_docker.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docker/utils/__init__.py b/docker/utils/__init__.py index 01fe268735dc..4d29d62425fa 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docker/utils/container_interface.py b/docker/utils/container_interface.py index 5db13e7d8ff0..f8b3eb07ee22 100644 --- a/docker/utils/container_interface.py +++ b/docker/utils/container_interface.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -29,16 +29,22 @@ def __init__( """Initialize the container interface with the given parameters. Args: - context_dir: The context directory for Docker operations. - profile: The profile name for the container. Defaults to "base". - yamls: A list of yaml files to extend ``docker-compose.yaml`` settings. These are extended in the order - they are provided. - envs: A list of environment variable files to extend the ``.env.base`` file. These are extended in the order - they are provided. - statefile: An instance of the :class:`Statefile` class to manage state variables. Defaults to None, in + context_dir: + The context directory for Docker operations. + profile: + The profile name for the container. Defaults to "base". + yamls: + A list of yaml files to extend ``docker-compose.yaml`` settings. These are extended in the order + they are provided. Defaults to None, in which case no additional yaml files are added. + envs: + A list of environment variable files to extend the ``.env.base`` file. These are extended in the order + they are provided. Defaults to None, in which case no additional environment variable files are added. + statefile: + An instance of the :class:`Statefile` class to manage state variables. Defaults to None, in which case a new configuration object is created by reading the configuration file at the path ``context_dir/.container.cfg``. - suffix: Optional docker image and container name suffix. Defaults to None, in which case, the docker name + suffix: + Optional docker image and container name suffix. Defaults to None, in which case, the docker name suffix is set to the empty string. A hyphen is inserted in between the profile and the suffix if the suffix is a nonempty string. For example, if "base" is passed to profile, and "custom" is passed to suffix, then the produced docker image and container will be named ``isaac-lab-base-custom``. @@ -68,8 +74,11 @@ def __init__( # insert a hyphen before the suffix if a suffix is given self.suffix = f"-{suffix}" - self.container_name = f"isaac-lab-{self.profile}{self.suffix}" - self.image_name = f"isaac-lab-{self.profile}{self.suffix}:latest" + # set names for easier reference + self.base_service_name = "isaac-lab-base" + self.service_name = f"isaac-lab-{self.profile}" + self.container_name = f"{self.service_name}{self.suffix}" + self.image_name = f"{self.service_name}{self.suffix}:latest" # keep the environment variables from the current environment, # except make sure that the docker name suffix is set from the script @@ -81,6 +90,26 @@ def __init__( # load the environment variables from the .env files self._parse_dot_vars() + def print_info(self): + """Print the container interface information.""" + print("=" * 60) + print(f"{'DOCKER CONTAINER INFO':^60}") # Centered title + print("=" * 60) + + print(f"{'Profile:':25} {self.profile}") + print(f"{'Suffix:':25} {self.suffix}") + print(f"{'Service Name:':25} {self.service_name}") + print(f"{'Image Name:':25} {self.image_name}") + print(f"{'Container Name:':25} {self.container_name}") + + print("-" * 60) + print(f"{'Docker Compose Arguments':^60}") + print("-" * 60) + print(f"{'YAMLs:':25} {' '.join(self.add_yamls)}") + print(f"{'Profiles:':25} {' '.join(self.add_profiles)}") + print(f"{'Env Files:':25} {' '.join(self.add_env_files)}") + print("=" * 60) + """ Operations. """ @@ -108,6 +137,33 @@ def does_image_exist(self) -> bool: result = subprocess.run(["docker", "image", "inspect", self.image_name], capture_output=True, text=True) return result.returncode == 0 + def build(self): + """Build the Docker image.""" + print("[INFO] Building the docker image for the profile 'base'...\n") + # build the image for the base profile + cmd = ( + ["docker", "compose"] + + ["--file", "docker-compose.yaml"] + + ["--profile", "base"] + + ["--env-file", ".env.base"] + + ["build", self.base_service_name] + ) + subprocess.run(cmd, check=False, cwd=self.context_dir, env=self.environ) + print("[INFO] Finished building the docker image for the profile 'base'.\n") + + # build the image for the profile + if self.profile != "base": + print(f"[INFO] Building the docker image for the profile '{self.profile}'...\n") + cmd = ( + ["docker", "compose"] + + self.add_yamls + + self.add_profiles + + self.add_env_files + + ["build", self.service_name] + ) + subprocess.run(cmd, check=False, cwd=self.context_dir, env=self.environ) + print(f"[INFO] Finished building the docker image for the profile '{self.profile}'.\n") + def start(self): """Build and start the Docker container using the Docker compose command.""" print( @@ -122,33 +178,24 @@ def start(self): # build the image for the base profile if not running base (up will build base already if profile is base) if self.profile != "base": - subprocess.run( - [ - "docker", - "compose", - "--file", - "docker-compose.yaml", - "--env-file", - ".env.base", - "build", - "isaac-lab-base", - ], - check=False, - cwd=self.context_dir, - env=self.environ, + cmd = ( + ["docker", "compose"] + + ["--file", "docker-compose.yaml"] + + ["--profile", "base"] + + ["--env-file", ".env.base"] + + ["build", self.base_service_name] ) + subprocess.run(cmd, check=False, cwd=self.context_dir, env=self.environ) - # build the image for the profile - subprocess.run( + # start the container and build the image if not available + cmd = ( ["docker", "compose"] + self.add_yamls + self.add_profiles + self.add_env_files - + ["up", "--detach", "--build", "--remove-orphans"], - check=False, - cwd=self.context_dir, - env=self.environ, + + ["up", "--detach", "--build", "--remove-orphans"] ) + subprocess.run(cmd, check=False, cwd=self.context_dir, env=self.environ) def enter(self): """Enter the running container by executing a bash shell. @@ -158,34 +205,29 @@ def enter(self): """ if self.is_container_running(): print(f"[INFO] Entering the existing '{self.container_name}' container in a bash session...\n") - subprocess.run([ - "docker", - "exec", - "--interactive", - "--tty", - *(["-e", f"DISPLAY={os.environ['DISPLAY']}"] if "DISPLAY" in os.environ else []), - f"{self.container_name}", - "bash", - ]) + cmd = ( + ["docker", "exec", "--interactive", "--tty"] + + (["-e", f"DISPLAY={os.environ['DISPLAY']}"] if "DISPLAY" in os.environ else []) + + [self.container_name, "bash"] + ) + subprocess.run(cmd) else: raise RuntimeError(f"The container '{self.container_name}' is not running.") def stop(self): - """Stop the running container using the Docker compose command. - - Raises: - RuntimeError: If the container is not running. - """ + """Stop the running container using the Docker compose command.""" if self.is_container_running(): print(f"[INFO] Stopping the launched docker container '{self.container_name}'...\n") - subprocess.run( - ["docker", "compose"] + self.add_yamls + self.add_profiles + self.add_env_files + ["down", "--volumes"], - check=False, - cwd=self.context_dir, - env=self.environ, + # stop running services + cmd = ( + ["docker", "compose"] + self.add_yamls + self.add_profiles + self.add_env_files + ["down", "--volumes"] ) + subprocess.run(cmd, check=False, cwd=self.context_dir, env=self.environ) else: - raise RuntimeError(f"Can't stop container '{self.container_name}' as it is not running.") + print( + f"[INFO] Can't stop container '{self.container_name}' as it is not running." + " To check if the container is running, run 'docker ps' or 'docker container ls'.\n" + ) def copy(self, output_dir: Path | None = None): """Copy artifacts from the running container to the host machine. @@ -223,15 +265,8 @@ def copy(self, output_dir: Path | None = None): # copy the artifacts for container_path, host_path in artifacts.items(): - subprocess.run( - [ - "docker", - "cp", - f"isaac-lab-{self.profile}{self.suffix}:{container_path}/", - f"{host_path}", - ], - check=False, - ) + cmd = ["docker", "cp", f"{self.container_name}:{container_path}/", host_path] + subprocess.run(cmd, check=False, cwd=self.context_dir, env=self.environ) print("\n[INFO] Finished copying the artifacts from the container.") else: raise RuntimeError(f"The container '{self.container_name}' is not running.") @@ -255,20 +290,16 @@ def config(self, output_yaml: Path | None = None): output = [] # run the docker compose config command to generate the configuration - subprocess.run( - ["docker", "compose"] + self.add_yamls + self.add_profiles + self.add_env_files + ["config"] + output, - check=False, - cwd=self.context_dir, - env=self.environ, - ) + cmd = ["docker", "compose"] + self.add_yamls + self.add_profiles + self.add_env_files + ["config"] + output + subprocess.run(cmd, check=False, cwd=self.context_dir, env=self.environ) """ Helper functions. """ def _resolve_image_extension(self, yamls: list[str] | None = None, envs: list[str] | None = None): - """ - Resolve the image extension by setting up YAML files, profiles, and environment files for the Docker compose command. + """Resolve the image extension by setting up YAML files, profiles, and environment files for the + Docker compose command. Args: yamls: A list of yaml files to extend ``docker-compose.yaml`` settings. These are extended in the order diff --git a/docker/utils/state_file.py b/docker/utils/state_file.py index 07304e594988..505f272f4101 100644 --- a/docker/utils/state_file.py +++ b/docker/utils/state_file.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docker/utils/x11_utils.py b/docker/utils/x11_utils.py index 687fbab33333..4d7d0e3639f3 100644 --- a/docker/utils/x11_utils.py +++ b/docker/utils/x11_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docker/x11.yaml b/docker/x11.yaml index 89d7b8c873d9..bd9b22f16b70 100644 --- a/docker/x11.yaml +++ b/docker/x11.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docs/conf.py b/docs/conf.py index 00d7af5ae592..248e14c3f89c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -20,14 +20,16 @@ sys.path.insert(0, os.path.abspath("../source/isaaclab")) sys.path.insert(0, os.path.abspath("../source/isaaclab/isaaclab")) +sys.path.insert(0, os.path.abspath("../source/isaaclab_assets")) +sys.path.insert(0, os.path.abspath("../source/isaaclab_assets/isaaclab_assets")) sys.path.insert(0, os.path.abspath("../source/isaaclab_tasks")) sys.path.insert(0, os.path.abspath("../source/isaaclab_tasks/isaaclab_tasks")) sys.path.insert(0, os.path.abspath("../source/isaaclab_rl")) sys.path.insert(0, os.path.abspath("../source/isaaclab_rl/isaaclab_rl")) sys.path.insert(0, os.path.abspath("../source/isaaclab_mimic")) sys.path.insert(0, os.path.abspath("../source/isaaclab_mimic/isaaclab_mimic")) -sys.path.insert(0, os.path.abspath("../source/isaaclab_assets")) -sys.path.insert(0, os.path.abspath("../source/isaaclab_assets/isaaclab_assets")) +sys.path.insert(0, os.path.abspath("../source/isaaclab_contrib")) +sys.path.insert(0, os.path.abspath("../source/isaaclab_contrib/isaaclab_contrib")) # -- Project information ----------------------------------------------------- @@ -124,11 +126,11 @@ "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), "trimesh": ("https://trimesh.org/", None), - "torch": ("https://pytorch.org/docs/stable/", None), + "torch": ("https://docs.pytorch.org/docs/stable/", None), "isaacsim": ("https://docs.isaacsim.omniverse.nvidia.com/5.1.0/py/", None), "gymnasium": ("https://gymnasium.farama.org/", None), "warp": ("https://nvidia.github.io/warp/", None), - "dev-guide": ("https://docs.omniverse.nvidia.com/dev-guide/latest", None), + "omniverse": ("https://docs.omniverse.nvidia.com/dev-guide/latest", None), } # Add any paths that contain templates here, relative to this directory. @@ -157,6 +159,7 @@ "omni.client", "omni.physx", "omni.physics", + "usdrt", "pxr.PhysxSchema", "pxr.PhysicsSchemaTools", "omni.replicator", @@ -190,6 +193,8 @@ "nvidia.srl", "flatdict", "IPython", + "cv2", + "imageio", "ipywidgets", "mpl_toolkits", ] diff --git a/docs/index.rst b/docs/index.rst index fbffccd68209..97b5f851e082 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,8 +36,8 @@ Isaac lab is developed with specific robot assets that are now **Batteries-inclu The platform is also designed so that you can add your own robots! Please refer to the :ref:`how-to` section for details. -For more information about the framework, please refer to the `paper `_ -:cite:`mittal2023orbit`. For clarifications on NVIDIA Isaac ecosystem, please check out the +For more information about the framework, please refer to the `technical report `_ +:cite:`mittal2025isaaclab`. For clarifications on NVIDIA Isaac ecosystem, please check out the :ref:`isaac-lab-ecosystem` section. .. figure:: source/_static/tasks.jpg @@ -51,25 +51,29 @@ License The Isaac Lab framework is open-sourced under the BSD-3-Clause license, with certain parts under Apache-2.0 license. Please refer to :ref:`license` for more details. -Acknowledgement -=============== -Isaac Lab development initiated from the `Orbit `_ framework. -We would appreciate if you would cite it in academic publications as well: +Citation +======== + +If you use Isaac Lab in your research, please cite our technical report: .. code:: bibtex - @article{mittal2023orbit, - author={Mittal, Mayank and Yu, Calvin and Yu, Qinxi and Liu, Jingzhou and Rudin, Nikita and Hoeller, David and Yuan, Jia Lin and Singh, Ritvik and Guo, Yunrong and Mazhar, Hammad and Mandlekar, Ajay and Babich, Buck and State, Gavriel and Hutter, Marco and Garg, Animesh}, - journal={IEEE Robotics and Automation Letters}, - title={Orbit: A Unified Simulation Framework for Interactive Robot Learning Environments}, - year={2023}, - volume={8}, - number={6}, - pages={3740-3747}, - doi={10.1109/LRA.2023.3270034} + @article{mittal2025isaaclab, + title={Isaac Lab: A GPU-Accelerated Simulation Framework for Multi-Modal Robot Learning}, + author={Mayank Mittal and Pascal Roth and James Tigue and Antoine Richard and Octi Zhang and Peter Du and Antonio Serrano-Muñoz and Xinjie Yao and René Zurbrügg and Nikita Rudin and Lukasz Wawrzyniak and Milad Rakhsha and Alain Denzler and Eric Heiden and Ales Borovicka and Ossama Ahmed and Iretiayo Akinola and Abrar Anwar and Mark T. Carlson and Ji Yuan Feng and Animesh Garg and Renato Gasoto and Lionel Gulich and Yijie Guo and M. Gussert and Alex Hansen and Mihir Kulkarni and Chenran Li and Wei Liu and Viktor Makoviychuk and Grzegorz Malczyk and Hammad Mazhar and Masoud Moghani and Adithyavairavan Murali and Michael Noseworthy and Alexander Poddubny and Nathan Ratliff and Welf Rehberg and Clemens Schwarke and Ritvik Singh and James Latham Smith and Bingjie Tang and Ruchik Thaker and Matthew Trepte and Karl Van Wyk and Fangzhou Yu and Alex Millane and Vikram Ramasamy and Remo Steiner and Sangeeta Subramanian and Clemens Volk and CY Chen and Neel Jawale and Ashwin Varghese Kuruttukulam and Michael A. Lin and Ajay Mandlekar and Karsten Patzwaldt and John Welsh and Huihua Zhao and Fatima Anes and Jean-Francois Lafleche and Nicolas Moënne-Loccoz and Soowan Park and Rob Stepinski and Dirk Van Gelder and Chris Amevor and Jan Carius and Jumyung Chang and Anka He Chen and Pablo de Heras Ciechomski and Gilles Daviet and Mohammad Mohajerani and Julia von Muralt and Viktor Reutskyy and Michael Sauter and Simon Schirm and Eric L. Shi and Pierre Terdiman and Kenny Vilella and Tobias Widmer and Gordon Yeoman and Tiffany Chen and Sergey Grizan and Cathy Li and Lotus Li and Connor Smith and Rafael Wiltz and Kostas Alexis and Yan Chang and David Chu and Linxi "Jim" Fan and Farbod Farshidian and Ankur Handa and Spencer Huang and Marco Hutter and Yashraj Narang and Soha Pouya and Shiwei Sheng and Yuke Zhu and Miles Macklin and Adam Moravanszky and Philipp Reist and Yunrong Guo and David Hoeller and Gavriel State}, + journal={arXiv preprint arXiv:2511.04831}, + year={2025}, + url={https://arxiv.org/abs/2511.04831} } +Acknowledgement +=============== + +Isaac Lab development initiated from the `Orbit `_ framework. +We gratefully acknowledge the authors of Orbit for their foundational contributions. + + Table of Contents ================= diff --git a/docs/licenses/dependencies/ruff-license.txt b/docs/licenses/dependencies/ruff-license.txt new file mode 100644 index 000000000000..655a0c76fc55 --- /dev/null +++ b/docs/licenses/dependencies/ruff-license.txt @@ -0,0 +1,430 @@ +MIT License + +Copyright (c) 2022 Charles Marsh + +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. + +end of terms and conditions + +The externally maintained libraries from which parts of the Software is derived +are: + +- autoflake, licensed as follows: + """ + Copyright (C) 2012-2018 Steven Myint + + 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. + """ + +- autotyping, licensed as follows: + """ + MIT License + + Copyright (c) 2023 Jelle Zijlstra + + 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. + """ + +- Flake8, licensed as follows: + """ + == Flake8 License (MIT) == + + Copyright (C) 2011-2013 Tarek Ziade + Copyright (C) 2012-2016 Ian Cordasco + + 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. + """ + +- flake8-eradicate, licensed as follows: + """ + MIT License + + Copyright (c) 2018 Nikita Sobolev + + 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. + """ + +- flake8-pyi, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2016 Łukasz Langa + + 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. + """ + +- flake8-simplify, licensed as follows: + """ + MIT License + + Copyright (c) 2020 Martin Thoma + + 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. + """ + +- isort, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2013 Timothy Edmund Crosley + + 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. + """ + +- pygrep-hooks, licensed as follows: + """ + Copyright (c) 2018 Anthony Sottile + + 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. + """ + +- pycodestyle, licensed as follows: + """ + Copyright © 2006-2009 Johann C. Rocholl + Copyright © 2009-2014 Florent Xicluna + Copyright © 2014-2020 Ian Lee + + Licensed under the terms of the Expat License + + 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. + """ + +- pydocstyle, licensed as follows: + """ + Copyright (c) 2012 GreenSteam, + + Copyright (c) 2014-2020 Amir Rachum, + + Copyright (c) 2020 Sambhav Kothari, + + 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. + """ + +- Pyflakes, licensed as follows: + """ + Copyright 2005-2011 Divmod, Inc. + Copyright 2013-2014 Florent Xicluna + + 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. + """ + +- Pyright, licensed as follows: + """ + MIT License + + Pyright - A static type checker for the Python language + Copyright (c) Microsoft Corporation. All rights reserved. + + 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 + """ + +- pyupgrade, licensed as follows: + """ + Copyright (c) 2017 Anthony Sottile + + 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. + """ + +- rome/tools, licensed under the MIT license: + """ + MIT License + + Copyright (c) Rome Tools, Inc. and its affiliates. + + 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. + """ + +- RustPython, licensed as follows: + """ + MIT License + + Copyright (c) 2020 RustPython Team + + 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. + """ + +- rust-analyzer/text-size, licensed under the MIT license: + """ + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + """ diff --git a/docs/licenses/dependencies/ruff-pre-commit-license.txt b/docs/licenses/dependencies/ruff-pre-commit-license.txt new file mode 100644 index 000000000000..c16d8bb1db26 --- /dev/null +++ b/docs/licenses/dependencies/ruff-pre-commit-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Astral Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/source/_static/demos/bin_packing.jpg b/docs/source/_static/demos/bin_packing.jpg new file mode 100644 index 000000000000..52d242d6cc4e Binary files /dev/null and b/docs/source/_static/demos/bin_packing.jpg differ diff --git a/docs/source/_static/demos/haply_teleop_franka.jpg b/docs/source/_static/demos/haply_teleop_franka.jpg new file mode 100644 index 000000000000..14461095c719 Binary files /dev/null and b/docs/source/_static/demos/haply_teleop_franka.jpg differ diff --git a/docs/source/_static/demos/multi-mesh-raycast.jpg b/docs/source/_static/demos/multi-mesh-raycast.jpg new file mode 100644 index 000000000000..b9d20d7150a1 Binary files /dev/null and b/docs/source/_static/demos/multi-mesh-raycast.jpg differ diff --git a/docs/source/_static/migration/ovd_pvd_comparison.jpg b/docs/source/_static/migration/ovd_pvd_comparison.jpg new file mode 100644 index 000000000000..f2d8fb31462f Binary files /dev/null and b/docs/source/_static/migration/ovd_pvd_comparison.jpg differ diff --git a/docs/source/_static/overview/sensors/tacsl_demo.jpg b/docs/source/_static/overview/sensors/tacsl_demo.jpg new file mode 100644 index 000000000000..a723d5ea8cb6 Binary files /dev/null and b/docs/source/_static/overview/sensors/tacsl_demo.jpg differ diff --git a/docs/source/_static/overview/sensors/tacsl_diagram.jpg b/docs/source/_static/overview/sensors/tacsl_diagram.jpg new file mode 100644 index 000000000000..323c9a77e8d8 Binary files /dev/null and b/docs/source/_static/overview/sensors/tacsl_diagram.jpg differ diff --git a/docs/source/_static/overview/sensors/tacsl_force_field_example.jpg b/docs/source/_static/overview/sensors/tacsl_force_field_example.jpg new file mode 100644 index 000000000000..eeca0295a2f0 Binary files /dev/null and b/docs/source/_static/overview/sensors/tacsl_force_field_example.jpg differ diff --git a/docs/source/_static/overview/sensors/tacsl_taxim_example.jpg b/docs/source/_static/overview/sensors/tacsl_taxim_example.jpg new file mode 100644 index 000000000000..37e8dab491b6 Binary files /dev/null and b/docs/source/_static/overview/sensors/tacsl_taxim_example.jpg differ diff --git a/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_anymal_d_v0_IO_descriptors.yaml b/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_anymal_d_v0_IO_descriptors.yaml index b9fa1a0f6c62..f44840c0f908 100644 --- a/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_anymal_d_v0_IO_descriptors.yaml +++ b/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_anymal_d_v0_IO_descriptors.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_g1_v0_IO_descriptors.yaml b/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_g1_v0_IO_descriptors.yaml index b01ffffef578..d932800eaf6e 100644 --- a/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_g1_v0_IO_descriptors.yaml +++ b/docs/source/_static/policy_deployment/01_io_descriptors/isaac_velocity_flat_g1_v0_IO_descriptors.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/docs/source/_static/policy_deployment/02_gear_assembly/gear_assembly_sim_real.webm b/docs/source/_static/policy_deployment/02_gear_assembly/gear_assembly_sim_real.webm new file mode 100644 index 000000000000..9a232dc0ceb2 Binary files /dev/null and b/docs/source/_static/policy_deployment/02_gear_assembly/gear_assembly_sim_real.webm differ diff --git a/docs/source/_static/policy_deployment/02_gear_assembly/sim_real_gear_assembly_train.jpg b/docs/source/_static/policy_deployment/02_gear_assembly/sim_real_gear_assembly_train.jpg new file mode 100644 index 000000000000..6bc1fc111043 Binary files /dev/null and b/docs/source/_static/policy_deployment/02_gear_assembly/sim_real_gear_assembly_train.jpg differ diff --git a/docs/source/_static/refs.bib b/docs/source/_static/refs.bib index c3c3819c42cc..8f696b6b36e1 100644 --- a/docs/source/_static/refs.bib +++ b/docs/source/_static/refs.bib @@ -129,6 +129,14 @@ @inproceedings{allshire2022transferring organization={IEEE} } +@article{mittal2025isaaclab, + title={Isaac Lab: A GPU-Accelerated Simulation Framework for Multi-Modal Robot Learning}, + author={Mayank Mittal and Pascal Roth and James Tigue and Antoine Richard and Octi Zhang and Peter Du and Antonio Serrano-Muñoz and Xinjie Yao and René Zurbrügg and Nikita Rudin and Lukasz Wawrzyniak and Milad Rakhsha and Alain Denzler and Eric Heiden and Ales Borovicka and Ossama Ahmed and Iretiayo Akinola and Abrar Anwar and Mark T. Carlson and Ji Yuan Feng and Animesh Garg and Renato Gasoto and Lionel Gulich and Yijie Guo and M. Gussert and Alex Hansen and Mihir Kulkarni and Chenran Li and Wei Liu and Viktor Makoviychuk and Grzegorz Malczyk and Hammad Mazhar and Masoud Moghani and Adithyavairavan Murali and Michael Noseworthy and Alexander Poddubny and Nathan Ratliff and Welf Rehberg and Clemens Schwarke and Ritvik Singh and James Latham Smith and Bingjie Tang and Ruchik Thaker and Matthew Trepte and Karl Van Wyk and Fangzhou Yu and Alex Millane and Vikram Ramasamy and Remo Steiner and Sangeeta Subramanian and Clemens Volk and CY Chen and Neel Jawale and Ashwin Varghese Kuruttukulam and Michael A. Lin and Ajay Mandlekar and Karsten Patzwaldt and John Welsh and Huihua Zhao and Fatima Anes and Jean-Francois Lafleche and Nicolas Moënne-Loccoz and Soowan Park and Rob Stepinski and Dirk Van Gelder and Chris Amevor and Jan Carius and Jumyung Chang and Anka He Chen and Pablo de Heras Ciechomski and Gilles Daviet and Mohammad Mohajerani and Julia von Muralt and Viktor Reutskyy and Michael Sauter and Simon Schirm and Eric L. Shi and Pierre Terdiman and Kenny Vilella and Tobias Widmer and Gordon Yeoman and Tiffany Chen and Sergey Grizan and Cathy Li and Lotus Li and Connor Smith and Rafael Wiltz and Kostas Alexis and Yan Chang and David Chu and Linxi "Jim" Fan and Farbod Farshidian and Ankur Handa and Spencer Huang and Marco Hutter and Yashraj Narang and Soha Pouya and Shiwei Sheng and Yuke Zhu and Miles Macklin and Adam Moravanszky and Philipp Reist and Yunrong Guo and David Hoeller and Gavriel State}, + journal={arXiv preprint arXiv:2511.04831}, + year={2025}, + url={https://arxiv.org/abs/2511.04831} +} + @article{mittal2023orbit, author={Mittal, Mayank and Yu, Calvin and Yu, Qinxi and Liu, Jingzhou and Rudin, Nikita and Hoeller, David and Yuan, Jia Lin and Singh, Ritvik and Guo, Yunrong and Mazhar, Hammad and Mandlekar, Ajay and Babich, Buck and State, Gavriel and Hutter, Marco and Garg, Animesh}, journal={IEEE Robotics and Automation Letters}, @@ -175,3 +183,25 @@ @InProceedings{mittal2024symmetry pages={7433-7439}, doi={10.1109/ICRA57147.2024.10611493} } + +@article{kulkarni2025aerialgym, + author={Kulkarni, Mihir and Rehberg, Welf and Alexis, Kostas}, + journal={IEEE Robotics and Automation Letters}, + title={Aerial Gym Simulator: A Framework for Highly Parallelized Simulation of Aerial Robots}, + year={2025}, + volume={10}, + number={4}, + pages={4093-4100}, + doi={10.1109/LRA.2025.3548507} +} + +@article{si2022taxim, + title={Taxim: An example-based simulation model for gelsight tactile sensors}, + author={Si, Zilin and Yuan, Wenzhen}, + journal={IEEE Robotics and Automation Letters}, + volume={7}, + number={2}, + pages={2361--2368}, + year={2022}, + publisher={IEEE} +} diff --git a/docs/source/_static/tasks/drone_arl/arl_robot_1_track_position_state_based.jpg b/docs/source/_static/tasks/drone_arl/arl_robot_1_track_position_state_based.jpg new file mode 100644 index 000000000000..61c8ecb2d5e5 Binary files /dev/null and b/docs/source/_static/tasks/drone_arl/arl_robot_1_track_position_state_based.jpg differ diff --git a/docs/source/_static/tasks/manipulation/openarm_bi_reach.jpg b/docs/source/_static/tasks/manipulation/openarm_bi_reach.jpg new file mode 100644 index 000000000000..12e6620027d6 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/openarm_bi_reach.jpg differ diff --git a/docs/source/_static/tasks/manipulation/openarm_uni_lift.jpg b/docs/source/_static/tasks/manipulation/openarm_uni_lift.jpg new file mode 100644 index 000000000000..b7bb7c3cecf9 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/openarm_uni_lift.jpg differ diff --git a/docs/source/_static/tasks/manipulation/openarm_uni_open_drawer.jpg b/docs/source/_static/tasks/manipulation/openarm_uni_open_drawer.jpg new file mode 100644 index 000000000000..0bec48a40194 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/openarm_uni_open_drawer.jpg differ diff --git a/docs/source/_static/tasks/manipulation/openarm_uni_reach.jpg b/docs/source/_static/tasks/manipulation/openarm_uni_reach.jpg new file mode 100644 index 000000000000..46128f7607be Binary files /dev/null and b/docs/source/_static/tasks/manipulation/openarm_uni_reach.jpg differ diff --git a/docs/source/_static/visualizers/newton_viz.jpg b/docs/source/_static/visualizers/newton_viz.jpg new file mode 100644 index 000000000000..9a37ad09fc37 Binary files /dev/null and b/docs/source/_static/visualizers/newton_viz.jpg differ diff --git a/docs/source/_static/visualizers/ov_viz.jpg b/docs/source/_static/visualizers/ov_viz.jpg new file mode 100644 index 000000000000..e172ae42ee9f Binary files /dev/null and b/docs/source/_static/visualizers/ov_viz.jpg differ diff --git a/docs/source/_static/visualizers/rerun_viz.jpg b/docs/source/_static/visualizers/rerun_viz.jpg new file mode 100644 index 000000000000..2263cfbccfed Binary files /dev/null and b/docs/source/_static/visualizers/rerun_viz.jpg differ diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index aef91e80df50..92eec5ed6f3f 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -36,6 +36,8 @@ The following modules are available in the ``isaaclab`` extension: lab/isaaclab.sim.converters lab/isaaclab.sim.schemas lab/isaaclab.sim.spawners + lab/isaaclab.sim.views + lab/isaaclab.sim.utils isaaclab_rl extension @@ -64,6 +66,20 @@ The following modules are available in the ``isaaclab_mimic`` extension: datagen envs +isaaclab_contrib extension +----------------------------- + +The following modules are available in the ``isaaclab_contrib`` extension: + +.. currentmodule:: isaaclab_contrib + +.. autosummary:: + :toctree: lab_contrib + + actuators + assets + mdp + sensors isaaclab_tasks extension ------------------------ diff --git a/docs/source/api/lab/isaaclab.devices.rst b/docs/source/api/lab/isaaclab.devices.rst index 1a2ed776d3b0..5f04c4733cf6 100644 --- a/docs/source/api/lab/isaaclab.devices.rst +++ b/docs/source/api/lab/isaaclab.devices.rst @@ -15,6 +15,7 @@ Se3Keyboard Se2SpaceMouse Se3SpaceMouse + HaplyDevice OpenXRDevice ManusVive isaaclab.devices.openxr.retargeters.GripperRetargeter @@ -47,11 +48,13 @@ Game Pad :members: :inherited-members: :show-inheritance: + :noindex: .. autoclass:: Se3Gamepad :members: :inherited-members: :show-inheritance: + :noindex: Keyboard -------- @@ -60,11 +63,13 @@ Keyboard :members: :inherited-members: :show-inheritance: + :noindex: .. autoclass:: Se3Keyboard :members: :inherited-members: :show-inheritance: + :noindex: Space Mouse ----------- @@ -73,11 +78,22 @@ Space Mouse :members: :inherited-members: :show-inheritance: + :noindex: .. autoclass:: Se3SpaceMouse :members: :inherited-members: :show-inheritance: + :noindex: + +Haply +----- + +.. autoclass:: HaplyDevice + :members: + :inherited-members: + :show-inheritance: + :noindex: OpenXR ------ @@ -86,6 +102,7 @@ OpenXR :members: :inherited-members: :show-inheritance: + :noindex: Manus + Vive ------------ @@ -94,6 +111,7 @@ Manus + Vive :members: :inherited-members: :show-inheritance: + :noindex: Retargeters ----------- @@ -102,18 +120,22 @@ Retargeters :members: :inherited-members: :show-inheritance: + :noindex: .. autoclass:: isaaclab.devices.openxr.retargeters.Se3AbsRetargeter :members: :inherited-members: :show-inheritance: + :noindex: .. autoclass:: isaaclab.devices.openxr.retargeters.Se3RelRetargeter :members: :inherited-members: :show-inheritance: + :noindex: .. autoclass:: isaaclab.devices.openxr.retargeters.GR1T2Retargeter :members: :inherited-members: :show-inheritance: + :noindex: diff --git a/docs/source/api/lab/isaaclab.sensors.rst b/docs/source/api/lab/isaaclab.sensors.rst index c30ed948f099..52dfd9bba5d1 100644 --- a/docs/source/api/lab/isaaclab.sensors.rst +++ b/docs/source/api/lab/isaaclab.sensors.rst @@ -31,6 +31,11 @@ RayCasterCfg RayCasterCamera RayCasterCameraCfg + MultiMeshRayCaster + MultiMeshRayCasterData + MultiMeshRayCasterCfg + MultiMeshRayCasterCamera + MultiMeshRayCasterCameraCfg Imu ImuCfg @@ -151,7 +156,40 @@ Ray-Cast Camera :members: :inherited-members: :show-inheritance: - :exclude-members: __init__, class_type + :exclude-members: __init__, class_type, OffsetCfg + +Multi-Mesh Ray-Cast Sensor +-------------------------- + +.. autoclass:: MultiMeshRayCaster + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: MultiMeshRayCasterData + :members: + :inherited-members: + :exclude-members: __init__ + +.. autoclass:: MultiMeshRayCasterCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type, OffsetCfg + +Multi-Mesh Ray-Cast Camera +-------------------------- + +.. autoclass:: MultiMeshRayCasterCamera + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: MultiMeshRayCasterCameraCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type, OffsetCfg, RaycastTargetCfg Inertia Measurement Unit ------------------------ diff --git a/docs/source/api/lab/isaaclab.sim.rst b/docs/source/api/lab/isaaclab.sim.rst index 86aff36c175a..b6b5c372bc17 100644 --- a/docs/source/api/lab/isaaclab.sim.rst +++ b/docs/source/api/lab/isaaclab.sim.rst @@ -56,10 +56,3 @@ Simulation Context Builder -------------------------- .. automethod:: simulation_context.build_simulation_context - -Utilities ---------- - -.. automodule:: isaaclab.sim.utils - :members: - :show-inheritance: diff --git a/docs/source/api/lab/isaaclab.sim.utils.rst b/docs/source/api/lab/isaaclab.sim.utils.rst new file mode 100644 index 000000000000..f27e574efb9a --- /dev/null +++ b/docs/source/api/lab/isaaclab.sim.utils.rst @@ -0,0 +1,57 @@ +isaaclab.sim.utils +================== + +.. automodule:: isaaclab.sim.utils + + .. rubric:: Submodules + + .. autosummary:: + + stage + queries + prims + transforms + semantics + legacy + +Stage +----- + +.. automodule:: isaaclab.sim.utils.stage + :members: + :show-inheritance: + +Queries +------- + +.. automodule:: isaaclab.sim.utils.queries + :members: + :show-inheritance: + +Prims +----- + +.. automodule:: isaaclab.sim.utils.prims + :members: + :show-inheritance: + +Transforms +---------- + +.. automodule:: isaaclab.sim.utils.transforms + :members: + :show-inheritance: + +Semantics +--------- + +.. automodule:: isaaclab.sim.utils.semantics + :members: + :show-inheritance: + +Legacy +------ + +.. automodule:: isaaclab.sim.utils.legacy + :members: + :show-inheritance: diff --git a/docs/source/api/lab/isaaclab.sim.views.rst b/docs/source/api/lab/isaaclab.sim.views.rst new file mode 100644 index 000000000000..3a5f9bdecfe9 --- /dev/null +++ b/docs/source/api/lab/isaaclab.sim.views.rst @@ -0,0 +1,17 @@ +isaaclab.sim.views +================== + +.. automodule:: isaaclab.sim.views + + .. rubric:: Classes + + .. autosummary:: + + XformPrimView + +XForm Prim View +--------------- + +.. autoclass:: XformPrimView + :members: + :show-inheritance: diff --git a/docs/source/api/lab/isaaclab.utils.rst b/docs/source/api/lab/isaaclab.utils.rst index 9ef89739f79b..5b352152e0b5 100644 --- a/docs/source/api/lab/isaaclab.utils.rst +++ b/docs/source/api/lab/isaaclab.utils.rst @@ -11,14 +11,20 @@ array assets buffers + datasets dict interpolation + logger math + mesh modifiers noise + seed + sensors string timer types + version warp .. Rubric:: Functions @@ -65,6 +71,14 @@ Buffer operations :inherited-members: :show-inheritance: +Datasets operations +~~~~~~~~~~~~~~~~~~~ + +.. automodule:: isaaclab.utils.datasets + :members: + :show-inheritance: + :exclude-members: __init__, func + Dictionary operations ~~~~~~~~~~~~~~~~~~~~~ @@ -81,6 +95,13 @@ Interpolation operations :inherited-members: :show-inheritance: +Logger operations +~~~~~~~~~~~~~~~~~ + +.. automodule:: isaaclab.utils.logger + :members: + :show-inheritance: + Math operations ~~~~~~~~~~~~~~~ @@ -89,6 +110,14 @@ Math operations :inherited-members: :show-inheritance: +Mesh operations +~~~~~~~~~~~~~~~ + +.. automodule:: isaaclab.utils.mesh + :members: + :imported-members: + :show-inheritance: + Modifier operations ~~~~~~~~~~~~~~~~~~~ @@ -110,6 +139,20 @@ Noise operations :show-inheritance: :exclude-members: __init__, func +Seed operations +~~~~~~~~~~~~~~~ + +.. automodule:: isaaclab.utils.seed + :members: + :show-inheritance: + +Sensor operations +~~~~~~~~~~~~~~~~~ + +.. automodule:: isaaclab.utils.sensors + :members: + :show-inheritance: + String operations ~~~~~~~~~~~~~~~~~ @@ -131,6 +174,13 @@ Type operations :members: :show-inheritance: +Version operations +~~~~~~~~~~~~~~~~~~ + +.. automodule:: isaaclab.utils.version + :members: + :show-inheritance: + Warp operations ~~~~~~~~~~~~~~~ diff --git a/docs/source/api/lab_contrib/isaaclab_contrib.actuators.rst b/docs/source/api/lab_contrib/isaaclab_contrib.actuators.rst new file mode 100644 index 000000000000..1171c31e5eaf --- /dev/null +++ b/docs/source/api/lab_contrib/isaaclab_contrib.actuators.rst @@ -0,0 +1,25 @@ +isaaclab_contrib.actuators +============================= + +.. automodule:: isaaclab_contrib.actuators + + .. rubric:: Classes + + .. autosummary:: + + Thruster + ThrusterCfg + +Thruster Actuator +----------------- + +.. autoclass:: Thruster + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ThrusterCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/lab_contrib/isaaclab_contrib.assets.rst b/docs/source/api/lab_contrib/isaaclab_contrib.assets.rst new file mode 100644 index 000000000000..24375f918c8f --- /dev/null +++ b/docs/source/api/lab_contrib/isaaclab_contrib.assets.rst @@ -0,0 +1,31 @@ +isaaclab_contrib.assets +========================== + +.. automodule:: isaaclab_contrib.assets + + .. rubric:: Classes + + .. autosummary:: + + Multirotor + MultirotorCfg + MultirotorData + +Multirotor Asset +---------------- + +.. autoclass:: Multirotor + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: MultirotorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +.. autoclass:: MultirotorData + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/source/api/lab_contrib/isaaclab_contrib.mdp.rst b/docs/source/api/lab_contrib/isaaclab_contrib.mdp.rst new file mode 100644 index 000000000000..32421ef9b1f8 --- /dev/null +++ b/docs/source/api/lab_contrib/isaaclab_contrib.mdp.rst @@ -0,0 +1,33 @@ +isaaclab_contrib.mdp +======================= + +.. automodule:: isaaclab_contrib.mdp + +Actions +------- + +.. automodule:: isaaclab_contrib.mdp.actions + + .. rubric:: Classes + + .. autosummary:: + + ThrustActionCfg + ThrustAction + +Thrust Action Configuration +--------------------------- + +.. autoclass:: isaaclab_contrib.mdp.actions.thrust_actions_cfg.ThrustActionCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Thrust Action Implementation +---------------------------- + +.. autoclass:: isaaclab_contrib.mdp.actions.thrust_actions.ThrustAction + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/source/api/lab_contrib/isaaclab_contrib.sensors.rst b/docs/source/api/lab_contrib/isaaclab_contrib.sensors.rst new file mode 100644 index 000000000000..efcdeffe239b --- /dev/null +++ b/docs/source/api/lab_contrib/isaaclab_contrib.sensors.rst @@ -0,0 +1,39 @@ +isaaclab_contrib.sensors +======================== + +.. automodule:: isaaclab_contrib.sensors + + .. rubric:: Classes + + .. autosummary:: + + VisuoTactileSensor + VisuoTactileSensorCfg + VisuoTactileSensorData + GelSightRenderCfg + + +Visuo-Tactile Sensor +-------------------- + +.. autoclass:: VisuoTactileSensor + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: VisuoTactileSensorData + :members: + :inherited-members: + :exclude-members: __init__ + +.. autoclass:: VisuoTactileSensorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +.. autoclass:: GelSightRenderCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ diff --git a/docs/source/api/lab_mimic/isaaclab_mimic.envs.rst b/docs/source/api/lab_mimic/isaaclab_mimic.envs.rst index 7b4146f5b608..ea8dc82d1615 100644 --- a/docs/source/api/lab_mimic/isaaclab_mimic.envs.rst +++ b/docs/source/api/lab_mimic/isaaclab_mimic.envs.rst @@ -7,20 +7,138 @@ .. autosummary:: - FrankaCubeStackIKRelMimicEnv - FrankaCubeStackIKRelMimicEnvCfg + isaaclab_mimic.envs.franka_stack_ik_rel_mimic_env.FrankaCubeStackIKRelMimicEnv + isaaclab_mimic.envs.franka_stack_ik_rel_mimic_env_cfg.FrankaCubeStackIKRelMimicEnvCfg + isaaclab_mimic.envs.franka_stack_ik_abs_mimic_env.FrankaCubeStackIKAbsMimicEnv + isaaclab_mimic.envs.franka_stack_ik_abs_mimic_env_cfg.FrankaCubeStackIKAbsMimicEnvCfg + isaaclab_mimic.envs.galbot_stack_rmp_rel_mimic_env.RmpFlowGalbotCubeStackRelMimicEnv + isaaclab_mimic.envs.galbot_stack_rmp_rel_mimic_env_cfg.RmpFlowGalbotLeftArmGripperCubeStackRelMimicEnvCfg + isaaclab_mimic.envs.galbot_stack_rmp_rel_mimic_env_cfg.RmpFlowGalbotRightArmSuctionCubeStackRelMimicEnvCfg + isaaclab_mimic.envs.galbot_stack_rmp_abs_mimic_env.RmpFlowGalbotCubeStackAbsMimicEnv + isaaclab_mimic.envs.galbot_stack_rmp_abs_mimic_env_cfg.RmpFlowGalbotLeftArmGripperCubeStackAbsMimicEnvCfg + isaaclab_mimic.envs.galbot_stack_rmp_abs_mimic_env_cfg.RmpFlowGalbotRightArmSuctionCubeStackAbsMimicEnvCfg + isaaclab_mimic.envs.pick_place_mimic_env.PickPlaceRelMimicEnv + isaaclab_mimic.envs.pick_place_mimic_env.PickPlaceAbsMimicEnv + isaaclab_mimic.envs.agibot_place_upright_mug_mimic_env_cfg.RmpFlowAgibotPlaceUprightMugMimicEnvCfg + isaaclab_mimic.envs.agibot_place_toy2box_mimic_env_cfg.RmpFlowAgibotPlaceToy2BoxMimicEnvCfg + +Franka Environments +------------------- Franka Cube Stack IK Rel Mimic Env ----------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: FrankaCubeStackIKRelMimicEnv +.. autoclass:: isaaclab_mimic.envs.franka_stack_ik_rel_mimic_env.FrankaCubeStackIKRelMimicEnv :members: :inherited-members: + :show-inheritance: Franka Cube Stack IK Rel Mimic Env Cfg --------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.franka_stack_ik_rel_mimic_env_cfg.FrankaCubeStackIKRelMimicEnvCfg + :members: + :inherited-members: + :show-inheritance: + +Franka Cube Stack IK Abs Mimic Env +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.franka_stack_ik_abs_mimic_env.FrankaCubeStackIKAbsMimicEnv + :members: + :inherited-members: + :show-inheritance: + +Franka Cube Stack IK Abs Mimic Env Cfg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.franka_stack_ik_abs_mimic_env_cfg.FrankaCubeStackIKAbsMimicEnvCfg + :members: + :inherited-members: + :show-inheritance: + +Galbot Environments +------------------- + +Galbot Cube Stack Rel Mimic Env (RmpFlow) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.galbot_stack_rmp_rel_mimic_env.RmpFlowGalbotCubeStackRelMimicEnv + :members: + :inherited-members: + :show-inheritance: + +Galbot Left Arm Gripper Cube Stack Rel Mimic Env Cfg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.galbot_stack_rmp_rel_mimic_env_cfg.RmpFlowGalbotLeftArmGripperCubeStackRelMimicEnvCfg + :members: + :inherited-members: + :show-inheritance: + +Galbot Right Arm Suction Cube Stack Rel Mimic Env Cfg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.galbot_stack_rmp_rel_mimic_env_cfg.RmpFlowGalbotRightArmSuctionCubeStackRelMimicEnvCfg + :members: + :inherited-members: + :show-inheritance: + +Galbot Cube Stack Abs Mimic Env (RmpFlow) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.galbot_stack_rmp_abs_mimic_env.RmpFlowGalbotCubeStackAbsMimicEnv + :members: + :inherited-members: + :show-inheritance: + +Galbot Left Arm Gripper Cube Stack Abs Mimic Env Cfg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.galbot_stack_rmp_abs_mimic_env_cfg.RmpFlowGalbotLeftArmGripperCubeStackAbsMimicEnvCfg + :members: + :inherited-members: + :show-inheritance: + +Galbot Right Arm Suction Cube Stack Abs Mimic Env Cfg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.galbot_stack_rmp_abs_mimic_env_cfg.RmpFlowGalbotRightArmSuctionCubeStackAbsMimicEnvCfg + :members: + :inherited-members: + :show-inheritance: + +Agibot Environments +------------------- + +Pick Place Rel Mimic Env +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.pick_place_mimic_env.PickPlaceRelMimicEnv + :members: + :inherited-members: + :show-inheritance: + +Pick Place Abs Mimic Env +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.pick_place_mimic_env.PickPlaceAbsMimicEnv + :members: + :inherited-members: + :show-inheritance: + +Agibot Place Upright Mug Mimic Env Cfg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: isaaclab_mimic.envs.agibot_place_upright_mug_mimic_env_cfg.RmpFlowAgibotPlaceUprightMugMimicEnvCfg + :members: + :inherited-members: + :show-inheritance: + +Agibot Place Toy2Box Mimic Env Cfg +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: FrankaCubeStackIKRelMimicEnvCfg +.. autoclass:: isaaclab_mimic.envs.agibot_place_toy2box_mimic_env_cfg.RmpFlowAgibotPlaceToy2BoxMimicEnvCfg :members: :inherited-members: :show-inheritance: diff --git a/docs/source/deployment/cloudxr_teleoperation_cluster.rst b/docs/source/deployment/cloudxr_teleoperation_cluster.rst index 9548e29eb70d..2b374bcbe548 100644 --- a/docs/source/deployment/cloudxr_teleoperation_cluster.rst +++ b/docs/source/deployment/cloudxr_teleoperation_cluster.rst @@ -16,7 +16,7 @@ System Requirements * **Recommended requirement**: Kubernetes cluster with a node that has at least 2 RTX PRO 6000 / L40 GPUs or equivalent .. note:: - If you are using DGX Spark, check `DGX Spark Limitations `_ for compatibility. + If you are using DGX Spark, check `DGX Spark Limitations `_ for compatibility. Software Dependencies --------------------- diff --git a/docs/source/deployment/cluster.rst b/docs/source/deployment/cluster.rst index 467fda90f46d..ab9e03874e7a 100644 --- a/docs/source/deployment/cluster.rst +++ b/docs/source/deployment/cluster.rst @@ -204,7 +204,7 @@ ANYmal rough terrain locomotion training can be executed with the following comm The above will, in addition, also render videos of the training progress and store them under ``isaaclab/logs`` directory. .. _Singularity: https://docs.sylabs.io/guides/2.6/user-guide/index.html -.. _ETH Zurich Euler: https://scicomp.ethz.ch/wiki/Euler +.. _ETH Zurich Euler: https://www.gdc-docs.ethz.ch/EulerManual/site/overview/ .. _PBS Official Site: https://openpbs.org/ .. _apptainer: https://apptainer.org/ .. _documentation: https://www.apptainer.org/docs/admin/main/installation.html#install-ubuntu-packages diff --git a/docs/source/deployment/docker.rst b/docs/source/deployment/docker.rst index 71849189326f..9dad372c9c7d 100644 --- a/docs/source/deployment/docker.rst +++ b/docs/source/deployment/docker.rst @@ -47,7 +47,7 @@ needed to run Isaac Lab inside a Docker container. A subset of these are summari Dockerfiles which end with something else, (i.e. ``Dockerfile.ros2``) build an `image extension <#isaac-lab-image-extensions>`_. * **docker-compose.yaml**: Creates mounts to allow direct editing of Isaac Lab code from the host machine that runs the container. It also creates several named volumes such as ``isaac-cache-kit`` to - store frequently re-used resources compiled by Isaac Sim, such as shaders, and to retain logs, data, and documents. + store frequently reused resources compiled by Isaac Sim, such as shaders, and to retain logs, data, and documents. * **.env.base**: Stores environment variables required for the ``base`` build process and the container itself. ``.env`` files which end with something else (i.e. ``.env.ros2``) define these for `image extension <#isaac-lab-image-extensions>`_. * **docker-compose.cloudxr-runtime.patch.yaml**: A patch file that is applied to enable CloudXR Runtime support for @@ -76,6 +76,7 @@ Running the Container The script ``container.py`` parallels basic ``docker compose`` commands. Each can accept an `image extension argument <#isaac-lab-image-extensions>`_, or else they will default to the ``base`` image extension. These commands are: +* **build**: This builds the image for the given profile. It does not bring up the container. * **start**: This builds the image and brings up the container in detached mode (i.e. in the background). * **enter**: This begins a new bash process in an existing Isaac Lab container, and which can be exited without bringing down the container. @@ -307,7 +308,7 @@ To pull the minimal Isaac Lab container, run: .. code:: bash - docker pull nvcr.io/nvidia/isaac-lab:2.3.0 + docker pull nvcr.io/nvidia/isaac-lab:2.3.2 To run the Isaac Lab container with an interactive bash session, run: @@ -323,7 +324,7 @@ To run the Isaac Lab container with an interactive bash session, run: -v ~/docker/isaac-sim/logs:/root/.nvidia-omniverse/logs:rw \ -v ~/docker/isaac-sim/data:/root/.local/share/ov/data:rw \ -v ~/docker/isaac-sim/documents:/root/Documents:rw \ - nvcr.io/nvidia/isaac-lab:2.3.0 + nvcr.io/nvidia/isaac-lab:2.3.2 To enable rendering through X11 forwarding, run: @@ -342,7 +343,7 @@ To enable rendering through X11 forwarding, run: -v ~/docker/isaac-sim/logs:/root/.nvidia-omniverse/logs:rw \ -v ~/docker/isaac-sim/data:/root/.local/share/ov/data:rw \ -v ~/docker/isaac-sim/documents:/root/Documents:rw \ - nvcr.io/nvidia/isaac-lab:2.3.0 + nvcr.io/nvidia/isaac-lab:2.3.2 To run an example within the container, run: diff --git a/docs/source/experimental-features/newton-physics-integration/index.rst b/docs/source/experimental-features/newton-physics-integration/index.rst index 19fe68dfbb3c..48d19caca874 100644 --- a/docs/source/experimental-features/newton-physics-integration/index.rst +++ b/docs/source/experimental-features/newton-physics-integration/index.rst @@ -38,8 +38,9 @@ until the framework has reached an official release. We appreciate your understa :titlesonly: installation + isaaclab_newton-beta-2 training-environments - newton-visualizer + visualization limitations-and-known-bugs solver-transitioning sim-to-sim diff --git a/docs/source/experimental-features/newton-physics-integration/installation.rst b/docs/source/experimental-features/newton-physics-integration/installation.rst index fc59e188b52f..be0f82632e66 100644 --- a/docs/source/experimental-features/newton-physics-integration/installation.rst +++ b/docs/source/experimental-features/newton-physics-integration/installation.rst @@ -3,13 +3,13 @@ Installation Installing the Newton physics integration branch requires three things: -1) Isaac sim 5.0 -2) The ``feature/newton`` branch of Isaac Lab -3) Ubuntu 22.04 or 24.04 (Windows will be supported soon) +1) The ``feature/newton`` branch of Isaac Lab +2) Ubuntu 22.04 or 24.04 (Windows will be supported soon) +3) [Optional] Isaac sim 5.1 (Isaac Sim is not required if the Omniverse visualizer is not used) To begin, verify the version of Isaac Sim by checking the title of the window created when launching the simulation app. Alternatively, you can find more explicit version information under the ``Help -> About`` menu within the app. -If your version is less than 5.0, you must first `update or reinstall Isaac Sim `_ before +If your version is less than 5.1, you must first `update or reinstall Isaac Sim `_ before you can proceed further. Next, navigate to the root directory of your local copy of the Isaac Lab repository and open a terminal. @@ -20,7 +20,7 @@ Make sure we are on the ``feature/newton`` branch by running the following comma git checkout feature/newton -Below, we provide instructions for installing Isaac Sim through pip or binary. +Below, we provide instructions for installing Isaac Sim through pip. Pip Installation @@ -28,6 +28,10 @@ Pip Installation We recommend using conda for managing your python environments. Conda can be downloaded and installed from `here `_. +If you previously already have a virtual environment for Isaac Lab, please ensure to start from a fresh environment to avoid any dependency conflicts. +If you have installed earlier versions of mujoco, mujoco-warp, or newton packages through pip, we recommend first +cleaning your pip cache with ``pip cache purge`` to remove any cache of earlier versions that may be conflicting with the latest. + Create a new conda environment: .. code-block:: bash @@ -46,35 +50,11 @@ Install the correct version of torch and torchvision: pip install -U torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 -Install Isaac Sim 5.0: - -.. code-block:: bash - - pip install "isaacsim[all,extscache]==5.0.0" --extra-index-url https://pypi.nvidia.com - -Install Isaac Lab extensions and dependencies: - -.. code-block:: bash - - ./isaaclab.sh -i - - -Binary Installation -------------------- - -Follow the Isaac Sim `documentation `_ to install Isaac Sim 5.0 binaries. - -Enter the Isaac Lab directory: - -.. code-block:: bash - - cd IsaacLab - -Add a symbolic link to the Isaac Sim installation: +[Optional] Install Isaac Sim 5.1: .. code-block:: bash - ln -s path_to_isaac_sim _isaac_sim + pip install "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.com Install Isaac Lab extensions and dependencies: @@ -91,3 +71,8 @@ To verify that the installation was successful, run the following command from t .. code-block:: bash ./isaaclab.sh -p scripts/environments/zero_agent.py --task Isaac-Cartpole-Direct-v0 --num_envs 128 + + +Note that since Newton requires a more recent version of Warp than Isaac Sim 5.1, there may be some incompatibility issues +that could result in errors such as ``ModuleNotFoundError: No module named 'warp.sim'``. These are ok to ignore and should not +impact usability. diff --git a/docs/source/experimental-features/newton-physics-integration/isaaclab_newton-beta-2.rst b/docs/source/experimental-features/newton-physics-integration/isaaclab_newton-beta-2.rst new file mode 100644 index 000000000000..2e7b4dd9ec42 --- /dev/null +++ b/docs/source/experimental-features/newton-physics-integration/isaaclab_newton-beta-2.rst @@ -0,0 +1,52 @@ +Isaac Lab - Newton Beta 2 +========================= + +Isaac Lab - Newton Beta 2 (feature/newton branch) provides Newton physics engine integration for Isaac Lab. We refactored our code so that we can not only support PhysX and Newton, but +any other physics engine, enabling users to bring their own physics engine to Isaac Lab if they desire. To enable this, we introduce base implementations of +our ``simulation interfaces``, :class:`~isaaclab.assets.articulation.Articulation` or :class:`~isaaclab.sensors.ContactSensor` for instance. These provide a +set of abstract methods that all physics engines must implement. In turn this allows all of the default Isaac Lab environments to work with any physics engine. +This also allows us to ensure that Isaac Lab - Newton Beta 2 is backwards compatible with Isaac Lab 2.X. For engine specific calls, users could get the underlying view of +the physics engine and call the engine specific APIs directly. + +However, as we are refactoring the code, we are also looking at ways to limit the overhead of Isaac Lab's. In an effort to minimize the overhead, we are moving +all our low level code away from torch, and instead will rely heavily on warp. This will allow us to write low level code that is more efficient, and also +to take advantage of the cuda-graphing. However, this means that the ``data classes`` such as :class:`~isaaclab.assets.articulation.ArticulationData` or +:class:`~isaaclab.sensors.ContactSensorData` will only return warp arrays. Users will hence have to call ``wp.to_torch`` to convert them to torch tensors if they desire. +Our setters/writers will support both warp arrays and torch tensors, and will use the most optimal strategy to update the warp arrays under the hood. This minimizes the +amount of changes required for users to migrate to Isaac Lab - Newton Beta 2. + +Another new feature of the writers and setters is the ability to provide them with masks and complete data (as opposed to indices and partial data in Isaac Lab 2.X). +Note that this feature will be available along with the ability to provide indices and partial data, and that the default behavior will still be to provide indices and partial data. +However, if using warp, users will have to provide masks and complete data. In general we encourage users to move to adopt this new feature as, if done well, it will +reduce on the fly memory allocations, and should result in better performance. + +On the optimization front, we decided to change quaternion conventions. Originally, Isaac Lab and Isaac Sim both adopted the ``wxyz`` convention. However, we were doing several +conversions to and from ``xyzw`` in our setters/writers as PhysX uses the ``xyzw`` convention. Since both Newton and Warp, also use the ``xyzw`` convention, we decided to change +our default convention to ``xyzw``. This means that all our APIs will now return quaternions in the ``xyzw`` convention. This is likely a breaking change for all the custom +mdps that are not using our :mod:`~isaaclab.utils.math` module. While this change is substantial, it should make things more consistent for when users are using the simulation +views directly, and will remove needless conversions. + +Finally, alongside the new isaaclab_newton extension, we are also introducing new isaaclab_experimental and isaaclab_task_experimental extensions. These extensions will allow +us to quickly bring new features to Isaac Lab main while giving them the time they need to mature before being fully integrated into the core Isaac Lab extensions. In this release, +we are introducing cuda-graphing support for direct rl tasks. This drastically reduces Isaac Lab's overhead making training faster. Try them out and let us know what you think! + +.. code-block:: bash + + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-Direct-Warp-v0 --num_envs 4096 --headless + +.. code-block:: bash + + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Ant-Direct-Warp-v0 --num_envs 4096 --headless + +.. code-block:: bash + + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Humanoid-Direct-Warp-v0 --num_envs 4096 --headless + + +What's Next? +============ + +Isaac Lab 3.0 is the upcoming release of Isaac Lab, which will be compatible with Isaac Sim 6.0, and at the same time will support the new Newton physics engine. +This will allow users to train policies on the Newton physics engine, or PhysX. To accommodate this major code refactoring are required. In this section, we +will go over some of the changes, how that will affect Isaac Lab 2.X users, and how to migrate to Isaac Lab 3.0. The current branch of ``feature/newton`` gives +a glance of what is to come. While the changes to the internal code structure are significant, the changes to the user API are minimal. diff --git a/docs/source/experimental-features/newton-physics-integration/limitations-and-known-bugs.rst b/docs/source/experimental-features/newton-physics-integration/limitations-and-known-bugs.rst index b7499042540e..e5eab3996d8a 100644 --- a/docs/source/experimental-features/newton-physics-integration/limitations-and-known-bugs.rst +++ b/docs/source/experimental-features/newton-physics-integration/limitations-and-known-bugs.rst @@ -48,7 +48,6 @@ Here is a non-exhaustive list of capabilities currently supported in the Newton * A1 * Go1 * Go2 - * Spot * Unitree G1 * Unitree H1 * Manipulation reach diff --git a/docs/source/experimental-features/newton-physics-integration/newton-visualizer.rst b/docs/source/experimental-features/newton-physics-integration/newton-visualizer.rst deleted file mode 100644 index 9efc7639bfb9..000000000000 --- a/docs/source/experimental-features/newton-physics-integration/newton-visualizer.rst +++ /dev/null @@ -1,39 +0,0 @@ -Newton Visualizer -================= - -Newton includes its own built-in visualizer to enable a fast and lightweight way to view the results of simulation. -Many additional features are planned for this system for the future, including the ability to view the results of -training remotely through a web browser. To enable use of the Newton Visualizer use the ``--newton_visualizer`` command line option. - -The Newton Visualizer is not capable of or intended to provide camera sensor data for robots being trained. It is solely -intended as a development debugging and visualization tool. - -It also currently only supports visualization of collision shapes, not visual shapes. - -Both the Omniverse RTX renderer and the Newton Visualizer can be run in parallel, or the Omniverse UI and RTX renderer -can be disabled using the ``--headless`` option. - -Using one of our training examples above, training the Cartpole environment, we might choose to disable the Omniverse UI -and RTX renderer using the ``--headless`` option and enable the Newton Visualizer instead as follows: - -.. code-block:: shell - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-Direct-v0 --num_envs 4096 --headless --newton_visualizer - -In general, we do not recommend using the Omniverse UI while training to ensure the fastest possible training times. -The Newton Visualizer has less of a performance penalty while running, and we aim to bring that overhead even lower in the future. - -If we would like to run the Omniverse UI and the Newton Visualizer at the same time, for example when running inference using a -lower number of environments, we can omit the ``--headless`` option while still adding the ``--newton_visualizer`` option, as follows: - -.. code-block:: shell - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/play.py --task Isaac-Cartpole-Direct-v0 --num_envs 128 --checkpoint logs/rsl_rl/cartpole_direct/2025-08-21_15-45-30/model_299.pt --newton_visualizer - -These options are available across all the learning frameworks. - -For more information about the Newton Visualizer, please refer to the `Newton documentation `_. - -IsaacLab provides additional customizations to the Newton visualizer with several learning-oriented features. These include the ability to pause rendering during training or pause the training process itself. Pausing rendering accelerates training by skipping rendering frames, which is particularly useful when we want to periodically check the trained policy without the performance overhead of continuous rendering. Pausing the training process is valuable for debugging purposes. Additionally, the visualizer's update frequency can be adjusted using a slider in the visualizer window, making it easy to prioritize rendering quality against training performance and vice-versa. - -All IsaacLab-specific customizations are organized under the *IsaacLab Training Controls* tab in the visualizer window. diff --git a/docs/source/experimental-features/newton-physics-integration/sim-to-real.rst b/docs/source/experimental-features/newton-physics-integration/sim-to-real.rst index 58957ba9d6d8..6b7a952a76c4 100644 --- a/docs/source/experimental-features/newton-physics-integration/sim-to-real.rst +++ b/docs/source/experimental-features/newton-physics-integration/sim-to-real.rst @@ -40,7 +40,7 @@ Train the teacher policy for the G1 velocity task using the Newton backend. The .. code-block:: bash - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Flat-G1-v1 --num_envs=4096 --headless + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Flat-G1-v1 --num_envs=4096 The teacher policy includes privileged observations (e.g., root linear velocity) defined in ``PolicyCfg(ObsGroup)``. @@ -59,7 +59,7 @@ Run the student distillation task ``Velocity-G1-Distillation-v1`` using ``--load .. code-block:: bash - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Velocity-G1-Distillation-v1 --num_envs=4096 --headless --load_run 2025-08-13_23-53-28 --checkpoint model_1499.pt + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Velocity-G1-Distillation-v1 --num_envs=4096 --load_run 2025-08-13_23-53-28 --checkpoint model_1499.pt .. note:: @@ -74,7 +74,7 @@ Use ``--load_run`` and ``--checkpoint`` to initialize from the distilled policy. .. code-block:: bash - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Velocity-G1-Student-Finetune-v1 --num_envs=4096 --headless --load_run 2025-08-20_16-06-52_distillation --checkpoint model_1499.pt + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Velocity-G1-Student-Finetune-v1 --num_envs=4096 --load_run 2025-08-20_16-06-52_distillation --checkpoint model_1499.pt This starts from the distilled student policy and improves it further with RL training. @@ -86,7 +86,7 @@ You can replay the student policy via: .. code-block:: bash - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/play.py --task=Velocity-G1-Student-Finetune-v1 --num_envs=32 + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/play.py --task=Velocity-G1-Student-Finetune-v1 --num_envs=32 --visualizer newton This exports the policy as ``.pt`` and ``.onnx`` files in the run's export directory, ready for real robot deployment. diff --git a/docs/source/experimental-features/newton-physics-integration/sim-to-sim.rst b/docs/source/experimental-features/newton-physics-integration/sim-to-sim.rst index 3ccc8807cc61..8eebbdffac3f 100644 --- a/docs/source/experimental-features/newton-physics-integration/sim-to-sim.rst +++ b/docs/source/experimental-features/newton-physics-integration/sim-to-sim.rst @@ -54,7 +54,8 @@ To run a PhysX-trained policy with the Newton backend, use this command template --task= \ --num_envs=32 \ --checkpoint \ - --policy_transfer_file + --policy_transfer_file \ + --visualizer newton Here are examples for different robots: @@ -66,8 +67,8 @@ Here are examples for different robots: --task=Isaac-Velocity-Flat-G1-v0 \ --num_envs=32 \ --checkpoint \ - --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_g1.yaml - + --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_g1.yaml \ + --visualizer newton 2. Unitree H1 @@ -78,7 +79,8 @@ Here are examples for different robots: --task=Isaac-Velocity-Flat-H1-v0 \ --num_envs=32 \ --checkpoint \ - --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_h1.yaml + --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_h1.yaml \ + --visualizer newton 3. Unitree Go2 @@ -89,7 +91,8 @@ Here are examples for different robots: --task=Isaac-Velocity-Flat-Go2-v0 \ --num_envs=32 \ --checkpoint \ - --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_go2.yaml + --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_go2.yaml \ + --visualizer newton 4. ANYmal-D @@ -101,7 +104,8 @@ Here are examples for different robots: --task=Isaac-Velocity-Flat-Anymal-D-v0 \ --num_envs=32 \ --checkpoint \ - --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_anymal_d.yaml + --policy_transfer_file scripts/sim2sim_transfer/config/physx_to_newton_anymal_d.yaml \ + --visualizer newton Note that to run this, you need to checkout the Newton-based branch of IsaacLab such as ``feature/newton``. diff --git a/docs/source/experimental-features/newton-physics-integration/training-environments.rst b/docs/source/experimental-features/newton-physics-integration/training-environments.rst index ef98339e5e60..5e25564a1360 100644 --- a/docs/source/experimental-features/newton-physics-integration/training-environments.rst +++ b/docs/source/experimental-features/newton-physics-integration/training-environments.rst @@ -6,9 +6,9 @@ To run training, we follow the standard Isaac Lab workflow. If you are new to Is The currently supported tasks are as follows: * Isaac-Cartpole-Direct-v0 -* Isaac-Cartpole-RGB-Camera-Direct-v0 (requires ``--enable_cameras``) -* Isaac-Cartpole-Depth-Camera-Direct-v0 (requires ``--enable_cameras``) * Isaac-Cartpole-v0 +* Isaac-Cartpole-RGB-Camera-Direct-v0 +* Isaac-Cartpole-Depth-Camera-Direct-v0 * Isaac-Ant-Direct-v0 * Isaac-Ant-v0 * Isaac-Humanoid-Direct-v0 @@ -23,11 +23,15 @@ The currently supported tasks are as follows: * Isaac-Velocity-Flat-Unitree-A1-v0 * Isaac-Velocity-Flat-Unitree-Go1-v0 * Isaac-Velocity-Flat-Unitree-Go2-v0 -* Isaac-Velocity-Flat-Spot-v0 * Isaac-Reach-Franka-v0 * Isaac-Reach-UR10-v0 * Isaac-Repose-Cube-Allegro-Direct-v0 +New experimental warp-based enviromnets: + +* Isaac-Cartpole-Direct-Warp-v0 +* Isaac-Ant-Direct-Warp-v0 +* Isaac-Humanoid-Direct-Warp-v0 To launch an environment and check that it loads as expected, we can start by trying it out with zero actions sent to its actuators. This can be done as follows, where ``TASK_NAME`` is the name of the task you’d like to run, and ``NUM_ENVS`` is the number of instances of the task that you’d like to create. @@ -51,19 +55,22 @@ To run the same environment with random actions we can use a different script: To train the environment we provide hooks to different rl frameworks. See the `Reinforcement Learning Scripts documentation `_ for more information. Here are some examples on how to run training on several different RL frameworks. Note that we are explicitly setting the number of environments to -4096 to benefit more from GPU parallelization. We also disable the Omniverse UI visualization to train the environment as quickly as possible by using the ``--headless`` option. +4096 to benefit more from GPU parallelization. + +By default, environments will train in headless mode. If visualization is required, use ``--visualizer`` and specify the desired visualizer. +Available options are ``newton``, ``rerun``, and ``omniverse`` (requires Isaac Sim installation). Note, multiple visualizers can be selected and launched. .. code-block:: shell - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-Direct-v0 --num_envs 4096 --headless + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-Direct-v0 --num_envs 4096 .. code-block:: shell - ./isaaclab.sh -p scripts/reinforcement_learning/skrl/train.py --task Isaac-Cartpole-Direct-v0 --num_envs 4096 --headless + ./isaaclab.sh -p scripts/reinforcement_learning/skrl/train.py --task Isaac-Cartpole-Direct-v0 --num_envs 4096 .. code-block:: shell - ./isaaclab.sh -p scripts/reinforcement_learning/rl_games/train.py --task Isaac-Cartpole-Direct-v0 --num_envs 4096 --headless + ./isaaclab.sh -p scripts/reinforcement_learning/rl_games/train.py --task Isaac-Cartpole-Direct-v0 --num_envs 4096 Once a policy is trained we can visualize it by using the play scripts. But first, we need to find the checkpoint of the trained policy. Typically, these are stored under: ``logs/NAME_OF_RL_FRAMEWORK/TASK_NAME/DATE``. @@ -71,11 +78,11 @@ Once a policy is trained we can visualize it by using the play scripts. But firs For instance with our rsl_rl example it could look like this: ``logs/rsl_rl/cartpole_direct/2025-08-21_15-45-30/model_299.pt`` -To then run this policy we can use the following command, note that we reduced the number of environments and removed the ``--headless`` option so that we can see our policy in action! +To then run this policy we can use the following command, note that we reduced the number of environments and added the ``--visualizer newton`` option so that we can see our policy in action! .. code-block:: shell - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/play.py --task Isaac-Cartpole-Direct-v0 --num_envs 128 --checkpoint logs/rsl_rl/cartpole_direct/2025-08-21_15-45-30/model_299.pt + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/play.py --task Isaac-Cartpole-Direct-v0 --num_envs 128 --visualizer newton --checkpoint logs/rsl_rl/cartpole_direct/2025-08-21_15-45-30/model_299.pt The same approach applies to all other frameworks. diff --git a/docs/source/experimental-features/newton-physics-integration/visualization.rst b/docs/source/experimental-features/newton-physics-integration/visualization.rst new file mode 100644 index 000000000000..f54435733934 --- /dev/null +++ b/docs/source/experimental-features/newton-physics-integration/visualization.rst @@ -0,0 +1,310 @@ +Visualization +============= + +.. currentmodule:: isaaclab + +Isaac Lab offers several lightweight visualizers for real-time simulation inspection and debugging. Unlike renderers that process sensor data, visualizers are meant for fast, interactive feedback. + +You can use any visualizer regardless of your chosen physics engine or rendering backend. + + +Overview +-------- + +Isaac Lab supports three visualizer backends, each optimized for different use cases: + +.. list-table:: Visualizer Comparison + :widths: 15 35 50 + :header-rows: 1 + + * - Visualizer + - Best For + - Key Features + * - **Omniverse** + - High-fidelity, Isaac Sim integration + - USD, visual markers, live plots + * - **Newton** + - Fast iteration + - Low overhead, visual markers + * - **Rerun** + - Remote viewing, replay + - Webviewer, time scrubbing, recording export + + +*The following visualizers are shown training the Isaac-Velocity-Flat-Anymal-D-v0 environment.* + +.. figure:: ../../_static/visualizers/ov_viz.jpg + :width: 100% + :alt: Omniverse Visualizer + + Omniverse Visualizer + +.. figure:: ../../_static/visualizers/newton_viz.jpg + :width: 100% + :alt: Newton Visualizer + + Newton Visualizer + +.. figure:: ../../_static/visualizers/rerun_viz.jpg + :width: 100% + :alt: Rerun Visualizer + + Rerun Visualizer + + +Quick Start +----------- + +Launch visualizers from the command line with ``--visualizer``: + +.. code-block:: bash + + # Launch all visualizers + python scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-v0 --visualizer omniverse newton rerun + + # Launch just newton visualizer + python scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-v0 --visualizer newton + + +If ``--headless`` is given, no visualizers will be launched. + +.. note:: + + The ``--headless`` argument may be deprecated in future versions to avoid confusion with the ``--visualizer`` + argument. For now, ``--headless`` takes precedence and disables all visualizers. + + +Configuration +~~~~~~~~~~~~~ + +Launching visualizers with the command line will use default visualizer configurations. Default configs can be found and edited in ``source/isaaclab/isaaclab/visualizers``. + +You can also configure custom visualizers in the code by defining new ``VisualizerCfg`` instances for the ``SimulationCfg``, for example: + +.. code-block:: python + + from isaaclab.sim import SimulationCfg + from isaaclab.visualizers import NewtonVisualizerCfg, OVVisualizerCfg, RerunVisualizerCfg + + sim_cfg = SimulationCfg( + visualizer_cfgs=[ + OVVisualizerCfg( + viewport_name="Visualizer Viewport", + create_viewport=True, + dock_position="SAME", + window_width=1280, + window_height=720, + camera_position=(0.0, 0.0, 20.0), # high top down view + camera_target=(0.0, 0.0, 0.0), + ), + NewtonVisualizerCfg( + camera_position=(5.0, 5.0, 5.0), # closer quarter view + camera_target=(0.0, 0.0, 0.0), + show_joints=True, + ), + RerunVisualizerCfg( + keep_historical_data=True, + keep_scalar_history=True, + record_to_rrd="my_training.rrd", + ), + ] + ) + + +Visualizer Backends +------------------- + +Omniverse Visualizer +~~~~~~~~~~~~~~~~~~~~ + +**Main Features:** + +- Native USD stage integration +- Visualization markers for debugging (arrows, frames, points, etc.) +- Live plots for monitoring training metrics +- Full Isaac Sim rendering capabilities and tooling + +**Core Configuration:** + +.. code-block:: python + + from isaaclab.visualizers import OVVisualizerCfg + + visualizer_cfg = OVVisualizerCfg( + # Viewport settings + viewport_name="Visualizer Viewport", # Viewport window name + create_viewport=True, # Create new viewport vs. use existing + dock_position="SAME", # Docking: 'LEFT', 'RIGHT', 'BOTTOM', 'SAME' + window_width=1280, # Viewport width in pixels + window_height=720, # Viewport height in pixels + + # Camera settings + camera_position=(8.0, 8.0, 3.0), # Initial camera position (x, y, z) + camera_target=(0.0, 0.0, 0.0), # Camera look-at target + + # Feature toggles + enable_markers=True, # Enable visualization markers + enable_live_plots=True, # Enable live plots (auto-expands frames) + ) + + +Newton Visualizer +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Main Features:** + +- Lightweight OpenGL rendering with low overhead +- Visualization markers (joints, contacts, springs, COM) +- Training and rendering pause controls +- Adjustable update frequency for performance tuning +- Some customizable rendering options (shadows, sky, wireframe) + + +**Interactive Controls:** + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Key/Input + - Action + * - **W, A, S, D** or **Arrow Keys** + - Forward / Left / Back / Right + * - **Q, E** + - Down / Up + * - **Left Click + Drag** + - Look around + * - **Mouse Scroll** + - Zoom in/out + * - **Space** + - Pause/resume rendering (physics continues) + * - **H** + - Toggle UI sidebar + * - **ESC** + - Exit viewer + +**Core Configuration:** + +.. code-block:: python + + from isaaclab.visualizers import NewtonVisualizerCfg + + visualizer_cfg = NewtonVisualizerCfg( + # Window settings + window_width=1920, # Window width in pixels + window_height=1080, # Window height in pixels + + # Camera settings + camera_position=(8.0, 8.0, 3.0), # Initial camera position (x, y, z) + camera_target=(0.0, 0.0, 0.0), # Camera look-at target + + # Performance tuning + update_frequency=1, # Update every N frames (1=every frame) + + # Physics debug visualization + show_joints=False, # Show joint visualizations + show_contacts=False, # Show contact points and normals + show_springs=False, # Show spring constraints + show_com=False, # Show center of mass markers + + # Rendering options + enable_shadows=True, # Enable shadow rendering + enable_sky=True, # Enable sky rendering + enable_wireframe=False, # Enable wireframe mode + + # Color customization + background_color=(0.53, 0.81, 0.92), # Sky/background color (RGB [0,1]) + ground_color=(0.18, 0.20, 0.25), # Ground plane color (RGB [0,1]) + light_color=(1.0, 1.0, 1.0), # Directional light color (RGB [0,1]) + ) + + +Rerun Visualizer +~~~~~~~~~~~~~~~~ + +**Main Features:** + +- Web viewer interface accessible from local or remote browser +- Metadata logging and filtering +- Recording to .rrd files for offline replay (.rrd files can be opened with ctrl+O from the web viewer) +- Timeline scrubbing and playback controls of recordings + +**Core Configuration:** + +.. code-block:: python + + from isaaclab.visualizers import RerunVisualizerCfg + + visualizer_cfg = RerunVisualizerCfg( + # Server settings + app_id="isaaclab-simulation", # Application identifier for viewer + web_port=9090, # Port for local web viewer (launched in browser) + + # Camera settings + camera_position=(8.0, 8.0, 3.0), # Initial camera position (x, y, z) + camera_target=(0.0, 0.0, 0.0), # Camera look-at target + + # History settings + keep_historical_data=False, # Keep transforms for time scrubbing + keep_scalar_history=False, # Keep scalar/plot history + + # Recording + record_to_rrd="recording.rrd", # Path to save .rrd file (None = no recording) + ) + + +Performance Note +---------------- + +To reduce overhead when visualizing large-scale environments, consider: + +- Using Newton instead of Omniverse or Rerun +- Reducing window sizes +- Higher update frequencies +- Pausing visualizers while they are not being used + + +Limitations +----------- + +**Rerun Visualizer Performance** + +The Rerun web-based visualizer may experience performance issues or crashes when visualizing large-scale +environments. For large-scale simulations, the Newton visualizer is recommended. Alternatively, to reduce load, +the num of environments can be overwritten and decreased using ``--num_envs``: + +.. code-block:: bash + + python scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-v0 --visualizer rerun --num_envs 512 + + +.. note:: + + A future feature will support visualizing only a subset of environments, which will improve visualization performance + and reduce resource usage while maintaining full-scale training in the background. + + +**Rerun Visualizer FPS Control** + +The FPS control in the Rerun visualizer UI may not affect the visualization frame rate in all configurations. + + +**Newton Visualizer Contact and Center of Mass Markers** + +Contact and center of mass markers are not yet supported in the Newton visualizer. This will be addressed in a future release. + + +**Newton Visualizer CUDA/OpenGL Interoperability Warnings** + +On some system configurations, the Newton visualizer may display warnings about CUDA/OpenGL interoperability: + +.. code-block:: text + + Warning: Could not get MSAA config, falling back to non-AA. + Warp CUDA error 999: unknown error (in function wp_cuda_graphics_register_gl_buffer) + Warp UserWarning: Could not register GL buffer since CUDA/OpenGL interoperability + is not available. Falling back to copy operations between the Warp array and the + OpenGL buffer. + +The visualizer will still function correctly but may experience reduced performance due to falling back to +CPU copy operations instead of direct GPU memory sharing. diff --git a/docs/source/features/multi_gpu.rst b/docs/source/features/multi_gpu.rst index a76db175e856..2537e5eff25b 100644 --- a/docs/source/features/multi_gpu.rst +++ b/docs/source/features/multi_gpu.rst @@ -141,14 +141,14 @@ For the master node, use the following command, where ``--nproc_per_node`` repre .. code-block:: shell - python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 scripts/reinforcement_learning/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --distributed + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --master_addr= --master_port=5555 scripts/reinforcement_learning/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --distributed .. tab-item:: rsl_rl :sync: rsl_rl .. code-block:: shell - python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --master_addr= --master_port=5555 scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed .. tab-item:: skrl :sync: skrl @@ -160,7 +160,7 @@ For the master node, use the following command, where ``--nproc_per_node`` repre .. code-block:: shell - python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 scripts/reinforcement_learning/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --master_addr= --master_port=5555 scripts/reinforcement_learning/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed .. tab-item:: JAX :sync: jax @@ -181,14 +181,14 @@ For non-master nodes, use the following command, replacing ``--node_rank`` with .. code-block:: shell - python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=ip_of_master_machine:5555 scripts/reinforcement_learning/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --distributed + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --master_addr= --master_port=5555 scripts/reinforcement_learning/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --distributed .. tab-item:: rsl_rl :sync: rsl_rl .. code-block:: shell - python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=ip_of_master_machine:5555 scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --master_addr= --master_port=5555 scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed .. tab-item:: skrl :sync: skrl @@ -200,7 +200,7 @@ For non-master nodes, use the following command, replacing ``--node_rank`` with .. code-block:: shell - python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=ip_of_master_machine:5555 scripts/reinforcement_learning/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --master_addr= --master_port=5555 scripts/reinforcement_learning/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed .. tab-item:: JAX :sync: jax diff --git a/docs/source/features/ray.rst b/docs/source/features/ray.rst index 959fb518eb50..0edf935e8389 100644 --- a/docs/source/features/ray.rst +++ b/docs/source/features/ray.rst @@ -65,7 +65,7 @@ The three following files contain the core functionality of the Ray integration. .. literalinclude:: ../../../scripts/reinforcement_learning/ray/tuner.py :language: python - :emphasize-lines: 18-54 + :emphasize-lines: 18-59 .. dropdown:: scripts/reinforcement_learning/ray/task_runner.py :icon: code diff --git a/docs/source/features/reproducibility.rst b/docs/source/features/reproducibility.rst index 9895a375d5d4..631e138376c9 100644 --- a/docs/source/features/reproducibility.rst +++ b/docs/source/features/reproducibility.rst @@ -10,7 +10,7 @@ or soft bodies. For more information, please refer to the `PhysX Determinism doc Based on above, Isaac Lab provides a deterministic simulation that ensures consistent simulation results across different runs. This is achieved by using the same random seed for the simulation environment and the physics engine. At construction of the environment, the random seed -is set to a fixed value using the :meth:`~isaacsim.core.utils.torch.set_seed` method. This method sets the +is set to a fixed value using the :meth:`~isaaclab.utils.seed.configure_seed` method. This method sets the random seed for both the CPU and GPU globally across different libraries, including PyTorch and NumPy. diff --git a/docs/source/how-to/cloudxr_teleoperation.rst b/docs/source/how-to/cloudxr_teleoperation.rst index 6411296116cb..e13b76305dcb 100644 --- a/docs/source/how-to/cloudxr_teleoperation.rst +++ b/docs/source/how-to/cloudxr_teleoperation.rst @@ -24,6 +24,18 @@ teleoperation in Isaac Lab. See :ref:`manus-vive-handtracking` for more information on supported hand-tracking peripherals. +.. note:: + + **Meta Quest 3 and Pico 4 Ultra Support (Early Access)** + + Meta Quest 3 and Pico 4 Ultra are now supported via the `CloudXR Early Access program`_. + Join the program by mentioning isaac use cases. Once approved, you'll receive email to set up NGC, + then download `CloudXR.js with Isaac Teleop samples`_ and follow its guide. + Pico 4 Ultra must use HTTPS mode (see NGC documentation for details). General availability + will be provided in a future version of Isaac Lab. + +.. _`CloudXR Early Access program`: https://developer.nvidia.com/cloudxr-sdk-early-access-program/join +.. _`CloudXR.js with Isaac Teleop samples`: https://catalog.ngc.nvidia.com/orgs/nvidia/resources/cloudxr-js-early-access?version=6.0.0-beta Overview -------- @@ -94,7 +106,7 @@ Prior to using CloudXR with Isaac Lab, please review the following system requir in the Apple Vision Pro being unable to find the Isaac Lab workstation on the network) .. note:: - If you are using DGX Spark, check `DGX Spark Limitations `_ for compatibility. + If you are using DGX Spark, check `DGX Spark Limitations `_ for compatibility. .. _`Omniverse Spatial Streaming`: https://docs.omniverse.nvidia.com/avp/latest/setup-network.html @@ -721,11 +733,11 @@ Here's an example of setting up hand tracking: # Create retargeters position_retargeter = Se3AbsRetargeter( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, zero_out_xy_rotation=True, use_wrist_position=False # Use pinch position (thumb-index midpoint) instead of wrist ) - gripper_retargeter = GripperRetargeter(bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT) + gripper_retargeter = GripperRetargeter(bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT) # Create OpenXR device with hand tracking and both retargeters device = OpenXRDevice( @@ -919,7 +931,7 @@ The retargeting system is designed to be extensible. You can create custom retar Any: The transformed control commands for the robot. """ # Access hand tracking data using TrackingTarget enum - right_hand_data = data[OpenXRDevice.TrackingTarget.HAND_RIGHT] + right_hand_data = data[DeviceBase.TrackingTarget.HAND_RIGHT] # Extract specific joint positions and orientations wrist_pose = right_hand_data.get("wrist") @@ -927,7 +939,7 @@ The retargeting system is designed to be extensible. You can create custom retar index_tip_pose = right_hand_data.get("index_tip") # Access head tracking data - head_pose = data[OpenXRDevice.TrackingTarget.HEAD] + head_pose = data[DeviceBase.TrackingTarget.HEAD] # Process the tracking data and apply your custom logic # ... @@ -935,18 +947,15 @@ The retargeting system is designed to be extensible. You can create custom retar # Return control commands in appropriate format return torch.tensor([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]) # Example output -3. Register your retargeter with the factory by adding it to the ``RETARGETER_MAP``: +3. Register your retargeter by setting ``retargeter_type`` on the config class: .. code-block:: python # Import your retargeter at the top of your module from my_package.retargeters import MyCustomRetargeter, MyCustomRetargeterCfg - # Add your retargeter to the factory - from isaaclab.devices.teleop_device_factory import RETARGETER_MAP - - # Register your retargeter type with its constructor - RETARGETER_MAP[MyCustomRetargeterCfg] = MyCustomRetargeter + # Link the config to the implementation for factory construction + MyCustomRetargeterCfg.retargeter_type = MyCustomRetargeter 4. Now you can use your custom retargeter in teleop device configurations: diff --git a/docs/source/how-to/haply_teleoperation.rst b/docs/source/how-to/haply_teleoperation.rst new file mode 100644 index 000000000000..1f8d1d6e2522 --- /dev/null +++ b/docs/source/how-to/haply_teleoperation.rst @@ -0,0 +1,240 @@ +.. _haply-teleoperation: + +Setting up Haply Teleoperation +=============================== + +.. currentmodule:: isaaclab + +`Haply Devices`_ provides haptic devices that enable intuitive robot teleoperation with +directional force feedback. The Haply Inverse3 paired with the VerseGrip creates an +end-effector control system with force feedback capabilities. + +Isaac Lab supports Haply devices for teleoperation workflows that require precise spatial +control with haptic feedback. This enables operators to feel contact forces during manipulation +tasks, improving control quality and task performance. + +This guide explains how to set up and use Haply devices with Isaac Lab for robot teleoperation. + +.. _Haply Devices: https://haply.co/ + + +Overview +-------- + +Using Haply with Isaac Lab involves the following components: + +* **Isaac Lab** simulates the robot environment and streams contact forces back to the operator + +* **Haply Inverse3** provides 3-DOF position tracking and force feedback in the operator's workspace + +* **Haply VerseGrip** adds orientation sensing and button inputs for gripper control + +* **Haply SDK** manages WebSocket communication between Isaac Lab and the Haply hardware + +This guide will walk you through: + +* :ref:`haply-system-requirements` +* :ref:`haply-installation` +* :ref:`haply-device-setup` +* :ref:`haply-running-demo` +* :ref:`haply-troubleshooting` + + +.. _haply-system-requirements: + +System Requirements +------------------- + +Hardware Requirements +~~~~~~~~~~~~~~~~~~~~~ + +* **Isaac Lab Workstation** + + * Ubuntu 22.04 or Ubuntu 24.04 + * Hardware requirements for 200Hz physics simulation: + + * CPU: 8-Core Intel Core i7 or AMD Ryzen 7 (or higher) + * Memory: 32GB RAM (64GB recommended) + * GPU: RTX 3090 or higher + + * Network: Same local network as Haply devices for WebSocket communication + +* **Haply Devices** + + * Haply Inverse3 - Haptic device for position tracking and force feedback + * Haply VerseGrip - Wireless controller for orientation and button inputs + * Both devices must be powered on and connected to the Haply SDK + +Software Requirements +~~~~~~~~~~~~~~~~~~~~~ + +* Isaac Lab (follow the :ref:`installation guide `) +* Haply SDK (provided by Haply Robotics) +* Python 3.10+ +* ``websockets`` Python package (automatically installed with Isaac Lab) + + +.. _haply-installation: + +Installation +------------ + +1. Install Isaac Lab +~~~~~~~~~~~~~~~~~~~~ + +Follow the Isaac Lab :ref:`installation guide ` to set up your environment. + +The ``websockets`` dependency is automatically included in Isaac Lab's requirements. + +2. Install Haply SDK +~~~~~~~~~~~~~~~~~~~~ + +Download the Haply SDK from the `Haply Devices`_ website. +Install the SDK software and configure the devices. + +3. Verify Installation +~~~~~~~~~~~~~~~~~~~~~~ + +Test that your Haply devices are detected by the Haply Device Manager. +You should see both Inverse3 and VerseGrip as connected. + + +.. _haply-device-setup: + +Device Setup +------------ + +1. Physical Setup +~~~~~~~~~~~~~~~~~ + +* Place the Haply Inverse3 on a stable surface +* Ensure the VerseGrip is charged and paired +* Position yourself comfortably to reach the Inverse3 workspace +* Keep the workspace clear of obstacles + +2. Start Haply SDK +~~~~~~~~~~~~~~~~~~ + +Launch the Haply SDK according to Haply's documentation. The SDK typically: + +* Runs a WebSocket server on ``localhost:10001`` +* Streams device data at 200Hz +* Displays connection status for both devices + +3. Test Communication +~~~~~~~~~~~~~~~~~~~~~ + +You can test the WebSocket connection using the following Python script: + +.. code:: python + + import asyncio + import websockets + import json + + async def test_haply(): + uri = "ws://localhost:10001" + async with websockets.connect(uri) as ws: + response = await ws.recv() + data = json.loads(response) + print("Inverse3:", data.get("inverse3", [])) + print("VerseGrip:", data.get("wireless_verse_grip", [])) + + asyncio.run(test_haply()) + +You should see device data streaming from both Inverse3 and VerseGrip. + + +.. _haply-running-demo: + +Running the Demo +---------------- + +The Haply teleoperation demo showcases robot manipulation with force feedback using +a Franka Panda arm. + +Basic Usage +~~~~~~~~~~~ + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Ensure Haply SDK is running + ./isaaclab.sh -p scripts/demos/haply_teleoperation.py --websocket_uri ws://localhost:10001 --pos_sensitivity 1.65 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + REM Ensure Haply SDK is running + isaaclab.bat -p scripts\demos\haply_teleoperation.py --websocket_uri ws://localhost:10001 --pos_sensitivity 1.65 + +The demo will: + +1. Connect to the Haply devices via WebSocket +2. Spawn a Franka Panda robot and a cube in simulation +3. Map Haply position to robot end-effector position +4. Stream contact forces back to the Inverse3 for haptic feedback + +Controls +~~~~~~~~ + +* **Move Inverse3**: Controls the robot end-effector position +* **VerseGrip Button A**: Open gripper +* **VerseGrip Button B**: Close gripper +* **VerseGrip Button C**: Rotate end-effector by 60° + +Advanced Options +~~~~~~~~~~~~~~~~ + +Customize the demo with command-line arguments: + +.. code:: bash + + # Use custom WebSocket URI + ./isaaclab.sh -p scripts/demos/haply_teleoperation.py \ + --websocket_uri ws://192.168.1.100:10001 + + # Adjust position sensitivity (default: 1.0) + ./isaaclab.sh -p scripts/demos/haply_teleoperation.py \ + --websocket_uri ws://localhost:10001 \ + --pos_sensitivity 2.0 + +Demo Features +~~~~~~~~~~~~~ + +* **Workspace Mapping**: Haply workspace is mapped to robot reachable space with safety limits +* **Inverse Kinematics**: Inverse Kinematics (IK) computes joint positions for desired end-effector pose +* **Force Feedback**: Contact forces from end-effector sensors are sent to Inverse3 for haptic feedback + + +.. _haply-troubleshooting: + +Troubleshooting +--------------- + +No Haptic Feedback +~~~~~~~~~~~~~~~~~~ + +**Problem**: No haptic feedback felt on Inverse3 + +Solutions: + +* Verify Inverse3 is the active device in Haply SDK +* Check contact forces are non-zero in simulation (try grasping the cube) +* Ensure ``limit_force`` is not set too low (default: 2.0N) + + +Next Steps +---------- + +* **Customize the demo**: Modify the workspace mapping or add custom button behaviors +* **Implement your own controller**: Use :class:`~isaaclab.devices.HaplyDevice` in your own scripts + +For more information on device APIs, see :class:`~isaaclab.devices.HaplyDevice` in the API documentation. diff --git a/docs/source/how-to/index.rst b/docs/source/how-to/index.rst index 2a4045d36d86..02c0ff99ae14 100644 --- a/docs/source/how-to/index.rst +++ b/docs/source/how-to/index.rst @@ -149,6 +149,18 @@ teleoperation in Isaac Lab. cloudxr_teleoperation +Setting up Haply Teleoperation +------------------------------ + +This guide explains how to use Haply Inverse3 and VerseGrip devices for robot teleoperation +with directional force feedback in Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + haply_teleoperation + + Understanding Simulation Performance ------------------------------------ diff --git a/docs/source/migration/comparing_simulation_isaacgym.rst b/docs/source/migration/comparing_simulation_isaacgym.rst new file mode 100644 index 000000000000..e70292f99aad --- /dev/null +++ b/docs/source/migration/comparing_simulation_isaacgym.rst @@ -0,0 +1,503 @@ +.. _migrating-from-isaacgymenvs-comparing-simulation: + +Comparing Simulations Between Isaac Gym and Isaac Lab +===================================================== + + +When migrating simulations from Isaac Gym to Isaac Lab, it is sometimes helpful to compare +the simulation configurations in Isaac Gym and Isaac Lab to identify differences between the two setups. +There may be differences in how default values are interpreted, how the importer treats certain +hierarchies of bodies, and how values are scaled. The only way to be certain that two simulations +are equivalent in the eyes of PhysX is to record a simulation trace of both setups and compare +them by inspecting them side-by-side. This approach works because PhysX is the same underlying +engine for both Isaac Gym and Isaac Lab, albeit with different versions. + + +Recording to PXD2 in Isaac Gym Preview Release +---------------------------------------------- + +Simulation traces in Isaac Gym can be recorded using the built-in PhysX Visual Debugger (PVD) +file output feature. Set the operating system environment variable ``GYM_PVD_FILE`` to the +desired output file path; the ``.pxd2`` file extension will be appended automatically. + +For detailed instructions, refer to the tuning documentation included with Isaac Gym: + +.. code-block:: text + + isaacgym/docs/_sources/programming/tuning.rst.txt + +.. note:: + + This file reference is provided because Isaac Gym does not have its documentation available online. + + +Recording to OVD in Isaac Lab +----------------------------- + +To record an OVD simulation trace file in Isaac Lab, you must set the appropriate Isaac Sim Kit +arguments. It is important that the ``omniPvdOvdRecordingDirectory`` variable is set **before** +``omniPvdOutputEnabled`` is set to ``true``. + +.. code-block:: bash + + ./isaaclab.sh -p scripts/benchmarks/benchmark_non_rl.py --task \ + --kit_args="--/persistent/physics/omniPvdOvdRecordingDirectory=/tmp/myovds/ \ + --/physics/omniPvdOutputEnabled=true" + +This example outputs a series of OVD files to the ``/tmp/myovds/`` directory. + +If the ``--kit_args`` argument does not work in your particular setup, you can set the Kit arguments +manually by editing the following file directly within the Isaac Sim source code: + +.. code-block:: text + + source/extensions/isaacsim.simulation_app/isaacsim/simulation_app/simulation_app.py + +Append the following lines after the ``args = []`` block: + +.. code-block:: python + + args.append("--/persistent/physics/omniPvdOvdRecordingDirectory=/path/to/output/ovds/") + args.append("--/physics/omniPvdOutputEnabled=true") + + +Inspecting PXD2 and OVD Files +----------------------------- + +By opening the PXD2 file in a PVD viewer and the OVD file in OmniPVD (a Kit extension), you can +manually compare the two simulation runs and their respective parameters. + +**PhysX Visual Debugger (PVD) for PXD2 Files** + +Download the PVD viewer from the NVIDIA Developer Tools page: + + ``_ + +Both version 2 and version 3 of the PVD viewer are compatible with PXD2 files. + +**OmniPVD for OVD Files** + +To view OVD files, enable the OmniPVD extension in the Isaac Sim application. For detailed +instructions, refer to the OmniPVD developer guide: + + https://docs.omniverse.nvidia.com/kit/docs/omni_physics/latest/extensions/ux/source/omni.physx.pvd/docs/dev_guide/physx_visual_debugger.html + +**Inspecting Contact Gizmos in OmniPVD** + +To inspect contact points between objects, enable the contact gizmos in OmniPVD. Ensure that the +simulation frame is set to **PRE** (pre-simulation frames of each simulation step) in the OmniPVD +timeline, or set the replay mode to **PRE**. This allows you to visualize contact information before +the solver processes each step. + +**Comparing PVD and OVD Files** + +Using the PVD viewer and the OmniPVD extension, you can now compare the simulations side-by-side +to identify configuration differences. On the left is PVD for PXD2 inspection and on the right is the OmniPVD +extension loaded to inspect OVD files. + +.. image:: ../_static/migration/ovd_pvd_comparison.jpg + + +Parameters to Verify During Simulation Comparison +------------------------------------------------- + +For PhysX articulations, each attribute is useful to inspect because it reveals how the link or shape +will actually behave in contact, under drives, and at constraints. Below, each attribute is expanded +with why it matters for debugging and tuning simulations. + + +PxArticulationLink +^^^^^^^^^^^^^^^^^^ + +Each link behaves like a rigid body with mass properties, damping, velocity limits, and contact-resolution +limits. Inspecting these helps explain stability issues, jitter, and odd responses to forces. + +Mass Properties +""""""""""""""" + +**Mass** + Determines how strongly the link accelerates under forces and how it shares impulses in collisions + and joint constraints. + + *When to inspect:* Understand why a link seems "too heavy" (barely moves when pushed) or "too light" + (flies around from small impulses), and to detect inconsistent mass distribution across a chain that + can cause unrealistic motion or joint stress. + +**Center of Mass (pose)** + Controls where forces effectively act and how the link balances. + + *When to inspect:* A character or mechanism tips over unexpectedly or feels unbalanced; an offset COM + can cause unrealistic torque for the same contact. + +**Inertia Tensor / Inertia Scale** + Defines rotational resistance about each axis. + + *When to inspect:* Links are too easy or too hard to spin relative to their mass, which affects joint + drive tuning and impact responses. + +Damping Properties +"""""""""""""""""" + +**Linear Damping** + Models velocity-proportional drag on translation; higher values make links lose linear speed faster. + + *When to inspect:* Links slide too far (damping too low) or feel "underwater" (damping too high), or + when articulation energy seems to vanish without obvious contact. + +**Angular Damping** + Models drag on rotation; higher values make spinning links slow more quickly. + + *When to inspect:* Links keep spinning after impacts or motor drives (too low), or joints feel "sticky" + and fail to swing freely under gravity (too high). + +Velocity Properties +""""""""""""""""""" + +**Linear Velocity** + Instantaneous world-space translational velocity of the link. + + *When to inspect:* Verify whether joint motors, gravity, or contacts are generating expected motion, + detect numerical explosions (huge spikes), and correlate with CCD thresholds and max linear velocity clamping. + +**Angular Velocity** + Instantaneous world-space rotational velocity. + + *When to inspect:* Verify joint drives, impacts, or constraints are producing the correct rotation; + spot runaway spin that can cause instability or tunneling before clamping takes effect. + +**Max Linear Velocity** + Upper bound PhysX uses to clamp linear speed before solving, intended to prevent numerical issues + from extremely fast motion. + + *When to inspect:* Objects start tunneling or simulations explode at high speeds. If too high, links + can move too far in one step; too low, they may appear unnaturally capped like "speed-limited" robots. + +**Max Angular Velocity** + Upper bound for angular speed; PhysX clamps angular velocity similarly to linear velocity. + + *When to inspect:* Links spin unrealistically fast after collisions or drives (value too large), or + rotation looks unnaturally limited, especially for wheels or rotors that should rotate quickly (value too small). + +Contact Resolution Properties +""""""""""""""""""""""""""""" + +**Max Depenetration Velocity** + Limits how much corrective velocity the solver may add in one step to resolve penetrations at contacts. + + *When to inspect:* Overlapping links "explode" outward or jitter after starting interpenetrating (too high), + or embedded links separate too slowly and appear stuck together (too low). + +**Max Contact Impulse** + Caps the impulse the solver can apply at contacts; per-body limit, with the actual contact limit being + the minimum of the two bodies' values. + + *When to inspect:* Contacts feel too soft (bodies interpenetrate deeply or sink into the environment) or + too rigid (sharp impulses causing ringing or bouncing), or when tuning "soft collisions" like rubber or + skin-like surfaces. + +State and Behavior Flags +"""""""""""""""""""""""" + +**Kinematic vs Dynamic flag / Disable gravity** + Indicates whether a link is driven kinematically or fully simulated, and whether gravity affects it. + + *When to inspect:* Parts appear frozen, snap directly to poses, or ignore gravity, which can drastically + change articulation behavior. + +**Sleep thresholds (linear, angular) and wake counter** + Control when a link is allowed to go to sleep and stop simulating. + + *When to inspect:* Articulations sleep too early (stopping motion) or never sleep (wasting performance + and causing low-amplitude jitter). + + +PxArticulationJoint +^^^^^^^^^^^^^^^^^^^ + +The inbound joint defines relative motion between a link and its parent. Inspecting motion and related +parameters explains limits, constraints, and how drives shape articulation pose and stability. + +Joint Configuration +""""""""""""""""""" + +**Motion** + Per-axis setting (locked, limited, free) that defines which degrees of freedom the joint allows and + whether ranges are restricted. + + *When to inspect:* A link moves in an unexpected direction (axis wrongly set to free), hits a hard stop + sooner or later than expected (limit vs locked), or seems unconstrained because an axis is mistakenly left free. + +**Joint Type / Axes definition** + Choice of revolute, prismatic, spherical, etc., and the local joint frames that define axes. + + *When to inspect:* A "hinge" behaves more like a ball joint or slides unexpectedly; incorrect type or + frame alignment easily produces weird motions. + +**Limits (swing, twist, linear)** + Specify allowed angular or linear ranges and often include stiffness/damping. + + *When to inspect:* Joints hyper-extend, clip through geometry, or suddenly snap at boundaries; mis-set + limits cause popping and instability. + +Drive Properties +"""""""""""""""" + +**Drive target position (orientation) and target velocity** + Desired relative pose and relative velocity that drives the articulation, often using spring-damper models. + + *When to inspect:* Controllers are too slow or overshoot and oscillate—target values and drive parameters + must match link mass and inertia. + +**Drive stiffness and damping (spring strength, tangential damping)** + Control how aggressively the joint tries to reach the target pose and how much overshoot is damped. + + *When to inspect:* Joints buzz or oscillate under load (stiffness high, damping low) or feel unresponsive + and "rubbery" (stiffness low). + +**Joint friction / resistance (if configured)** + Adds resistance even without explicit damping in drives. + + *When to inspect:* Passive joints keep swinging too long, or appear stuck even without drives. + + +PxShape +^^^^^^^ + +Shapes attached to links determine collision representation and contact behavior. Even if they are internal +in OmniPhysics, their properties have a strong impact on stability, contact timing, and visual alignment. + +Collision Offsets +""""""""""""""""" + +**Rest Offset** + Distance at which two shapes come to rest; sum of their rest offsets defines the separation where they "settle". + + *When to inspect:* Graphics and collision appear misaligned (gaps or visible intersections), or sliding + over meshes is rough. Small positive offsets can smooth sliding, while zero offset tends to align exactly + but may catch on geometry. + +**Contact Offset** + Distance at which contact generation begins; shapes whose distance is less than the sum of contact offsets + generate contacts. + + *When to inspect:* Contacts appear "too early" (objects seem to collide before visually touching, increasing + contact count) or "too late" (tunneling or jitter). The difference between contact and rest offsets is + crucial for predictive, stable contacts. + +Geometry and Materials +"""""""""""""""""""""" + +**Geometry type and dimensions** + Box, sphere, capsule, convex, mesh, and the associated size parameters. + + *When to inspect:* Collision footprint does not match the visual mesh—overly large shapes cause premature + contacts; small shapes allow visual intersection and change leverage at contacts. + +**Material(s): friction, restitution, compliance** + Friction coefficients and restitution define sliding and bounciness. + + *When to inspect:* An articulation foot skids too easily, sticks to the ground, or bounces unexpectedly. + Wrong materials can make mechanisms unstable or unresponsive. + +Shape Flags +""""""""""" + +**Flag for simulation / query / trigger** + Whether the shape participates in simulation contacts, raycasts only, or trigger events. + + *When to inspect:* Contacts do not appear (shape set as query only) or triggers unexpectedly create + physical collisions. + +**Contact density (CCD flags, if used)** + Continuous collision detection flags affecting how fast-moving links are handled. + + *When to inspect:* Fast articulation parts tunnel through thin obstacles, or CCD is too aggressive and + reduces performance. + + +PxRigidDynamic +^^^^^^^^^^^^^^ + +``PxRigidDynamic`` is the core simulated rigid body type in PhysX, so inspecting its attributes is crucial +for understanding individual object behavior, stability, and performance in the scene. Many attributes +mirror ``PxArticulationLink``, but a rigid dynamic is not constrained by articulation joints and can also +be used in kinematic mode. + +Mass and Mass-Related Properties +"""""""""""""""""""""""""""""""" + +**Mass** + Controls translational response to forces and impulses; for the same impulse, lower mass gives higher + velocity change. + + *When to inspect:* An object barely reacts to hits (mass too large) or flies away from small forces + (mass too small), or mass ratios between interacting bodies cause overly dominant or easily bullied bodies. + +**Center of Mass (COM) pose** + Defines where forces effectively act and around which point the body rotates. + + *When to inspect:* Objects tip over unexpectedly, roll in unintuitive ways, or feel "unbalanced." A COM + too high or off-center can cause strong torques from small contacts. + +**Inertia tensor / inertia scaling** + Determines resistance to angular acceleration around each axis for a given torque. + + *When to inspect:* Bodies are too easy or too hard to spin (e.g., a large object spinning quickly from + small hits), or when anisotropic behavior is needed (e.g., wheels that spin easily around one axis but + resist others). + +Damping and Velocity Limits +""""""""""""""""""""""""""" + +**Linear Damping** + Adds velocity-proportional drag on translation. + + *When to inspect:* Bodies slide too far or for too long (damping too low) or appear as if moving through + thick fluid (damping too high), and when scenes lose energy faster than friction alone would suggest. + +**Angular Damping** + Adds drag on rotation, reducing angular velocity over time. + + *When to inspect:* Spinning objects never settle or spin unrealistically long (too low), or they stop + rotating almost immediately after impact or motor impulses (too high). + +**Linear Velocity** + Current translational velocity used by the integrator and solver. + + *When to inspect:* Debug impulses, gravity, or applied forces to see whether the body is accelerating + as expected; detect spikes or non-physical jumps in speed. + +**Angular Velocity** + Current rotational speed around each axis. + + *When to inspect:* Rotations look jittery, explode numerically, or fail to respond to applied torques. + High values relative to time step and object scale can indicate instability. + +**Max Linear Velocity** + Upper bound used to clamp linear velocity before solving. + + *When to inspect:* Very fast bodies cause tunneling or simulation explosions (value too high), or they + appear unnaturally "speed-limited," especially projectiles or debris in high-energy scenes (value too low). + +**Max Angular Velocity** + Upper bound used to clamp angular velocity. + + *When to inspect:* Thin or small bodies spin so fast they destabilize the scene (value too high), or + spinning elements such as wheels, propellers, or debris appear artificially capped (value too low). + +Contact Resolution and Impulses +""""""""""""""""""""""""""""""" + +**Max Depenetration Velocity** + Limits the corrective velocity the solver may introduce in one step to resolve interpenetrations. + + *When to inspect:* Intersecting bodies "explode" apart or jitter violently after overlap (too high), or + separate very slowly and appear stuck or interpenetrated for several frames (too low). + +**Max Contact Impulse** + Caps the impulse that can be applied at contacts involving this body; the effective limit is the minimum + between the two bodies, or the dynamic body for static–dynamic contacts. + + *When to inspect:* Create softer contacts (lower limit) or very rigid, almost unyielding bodies (high or + default limit); objects sink into each other or bounce unrealistically. + +Sleep and Activation Behavior +""""""""""""""""""""""""""""""" + +**Sleep Threshold** + Mass-normalized kinetic energy below which a body becomes a candidate for sleeping. + + *When to inspect:* Bodies fall asleep too early while they should still move (threshold too high) or + constantly jitter and never sleep (threshold too low), which can hurt performance. + +**Wake Counter / isSleeping flag** + Internal timer and state indicating whether the body is active. + + *When to inspect:* Bodies refuse to wake up on interactions or wake too easily. Bad sleep behavior can + make scenes feel "dead" or too noisy. + +Kinematic Mode and Locking +"""""""""""""""""""""""""" + +**Kinematic Flag (PxRigidBodyFlag::eKINEMATIC)** + When set, the body is moved by ``setKinematicTarget`` and ignores forces and gravity, while still + affecting dynamic bodies it touches. + + *When to inspect:* Objects appear to have infinite mass (pushing others but not reacting) or ignore + gravity and impulses. Mismatched expectations here commonly cause odd behavior in characters, moving + platforms, or doors. + +**Rigid Dynamic Lock Flags (PxRigidDynamicLockFlag)** + Per-axis linear and angular DOF locks, effectively constraining motion without a joint. + + *When to inspect:* Bodies unexpectedly move in constrained directions (lock not set) or fail to + move/rotate where they should (lock set by mistake), especially for 2D-style movement or simple + constrained mechanisms. + +**Disable Gravity (PxActorFlag::eDISABLE_GRAVITY)** + Toggles whether the body is affected by scene gravity. + + *When to inspect:* Objects float in mid-air or drop unexpectedly. A common source of confusion in + mixed setups with some gravity-less bodies. + +Forces and Solver Overrides +""""""""""""""""""""""""""" + +**Applied force and torque (accumulated per step)** + Net forces/torques that will be integrated into velocity. + + *When to inspect:* Debug gameplay forces (thrusters, character pushes, explosions) to see if the expected + input is actually reaching the body. + +**Per-body solver iteration counts (minPositionIters, minVelocityIters)** + Overrides for how many solver iterations this body gets in constraints and contacts. + + *When to inspect:* Certain bodies (e.g., characters, stacked crates, fragile structures) need higher + stability or more accurate stacking. Low iterations can cause jitter and penetration; too high wastes + performance. + +Shape-Related Aspects +""""""""""""""""""""" + +While not properties of ``PxRigidDynamic`` itself, the shapes attached to it heavily influence behavior. + +**Attached Shapes' Rest and Contact Offsets** + Control predictive contact generation and visual separation as described earlier. + + *When to inspect:* A dynamic body seems to collide too early/late or appears to float above surfaces + or intersect them visually. + +**Attached Materials (friction, restitution)** + Define sliding and bounciness for this body's contacts. + + *When to inspect:* Rigid dynamics skid, stick, or bounce in unexpected ways. Often the "behavior issue" + is material configuration rather than mass or damping. + + +Summary: What to Inspect and Why +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The table below summarizes the key inspection areas for each PhysX component: + +.. list-table:: + :header-rows: 1 + :widths: 20 40 40 + + * - Component + - Key Attributes + - Debugging Focus + * - **Links** + - Mass, damping, velocities, limits + - Overall energy, stability, and response to joints/contacts + * - **Joints** + - Motion, limits, drives + - How articulation pose evolves; over/under-constrained motion + * - **Shapes** + - Offsets, materials, geometry + - Contact timing, friction behavior, visual vs physical alignment + * - **Rigid Dynamics** + - Mass, inertia, damping, velocity limits, sleep, kinematic flags + - Acceleration, settling, extreme motion, body state + +All of these attributes together provide a comprehensive picture of why an articulation or rigid body +behaves as it does and where to adjust parameters for stability, realism, or control performance. diff --git a/docs/source/migration/migrating_from_isaacgymenvs.rst b/docs/source/migration/migrating_from_isaacgymenvs.rst index 0346ef76d791..db6371c40c9d 100644 --- a/docs/source/migration/migrating_from_isaacgymenvs.rst +++ b/docs/source/migration/migrating_from_isaacgymenvs.rst @@ -355,6 +355,10 @@ of ``1/deg`` in the Isaac Sim UI but ``1/rad`` in Isaac Gym Preview Release. - 100.0 (rad) +For more details on performing thorough simulation comparisons between Isaac Gym and Isaac Lab, +please refer to the :ref:`migrating-from-isaacgymenvs-comparing-simulation` section. + + Cloner ------ @@ -924,6 +928,15 @@ To launch inferencing in Isaac Lab, use the command: python scripts/reinforcement_learning/rl_games/play.py --task=Isaac-Cartpole-Direct-v0 --num_envs=25 --checkpoint= +Additional Resources +~~~~~~~~~~~~~~~~~~~~ + +.. toctree:: + :maxdepth: 1 + + comparing_simulation_isaacgym + + .. _IsaacGymEnvs: https://github.com/isaac-sim/IsaacGymEnvs .. _Isaac Gym Preview Release: https://developer.nvidia.com/isaac-gym .. _release notes: https://github.com/isaac-sim/IsaacLab/releases diff --git a/docs/source/overview/core-concepts/sensors/index.rst b/docs/source/overview/core-concepts/sensors/index.rst index 31baaa9258b1..d2c63f212b76 100644 --- a/docs/source/overview/core-concepts/sensors/index.rst +++ b/docs/source/overview/core-concepts/sensors/index.rst @@ -19,3 +19,4 @@ The following pages describe the available sensors in more detail: frame_transformer imu ray_caster + visuo_tactile_sensor diff --git a/docs/source/overview/core-concepts/sensors/visuo_tactile_sensor.rst b/docs/source/overview/core-concepts/sensors/visuo_tactile_sensor.rst new file mode 100644 index 000000000000..d761c4a2e8a8 --- /dev/null +++ b/docs/source/overview/core-concepts/sensors/visuo_tactile_sensor.rst @@ -0,0 +1,205 @@ +.. _overview_sensors_tactile: + +.. currentmodule:: isaaclab + +Visuo-Tactile Sensor +==================== + + +The visuo-tactile sensor in Isaac Lab provides realistic tactile feedback through integration with TacSL (Tactile Sensor Learning) [Akinola2025]_. It is designed to simulate high-fidelity tactile interactions, generating both visual and force-based data that mirror real-world tactile sensors like GelSight devices. The sensor can provide tactile RGB images, force field distributions, and other intermediate tactile measurements essential for robotic manipulation tasks requiring fine tactile feedback. + + +.. figure:: ../../../_static/overview/sensors/tacsl_diagram.jpg + :align: center + :figwidth: 100% + :alt: Tactile sensor with RGB visualization and force fields + + +Configuration +~~~~~~~~~~~~~ + +Tactile sensors require specific configuration parameters to define their behavior and data collection properties. The sensor can be configured with various parameters including sensor resolution, force sensitivity, and output data types. + +.. code-block:: python + + from isaaclab.sensors import TiledCameraCfg + from isaaclab_assets.sensors import GELSIGHT_R15_CFG + import isaaclab.sim as sim_utils + + from isaaclab_contrib.sensors.tacsl_sensor import VisuoTactileSensorCfg + + # Tactile sensor configuration + tactile_sensor = VisuoTactileSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/elastomer/tactile_sensor", + ## Sensor configuration + render_cfg=GELSIGHT_R15_CFG, + enable_camera_tactile=True, + enable_force_field=True, + ## Elastomer configuration + tactile_array_size=(20, 25), + tactile_margin=0.003, + ## Contact object configuration + contact_object_prim_path_expr="{ENV_REGEX_NS}/contact_object", + ## Force field physics parameters + normal_contact_stiffness=1.0, + friction_coefficient=2.0, + tangential_stiffness=0.1, + ## Camera configuration + camera_cfg=TiledCameraCfg( + prim_path="{ENV_REGEX_NS}/Robot/elastomer_tip/cam", + update_period=1 / 60, # 60 Hz + height=320, + width=240, + data_types=["distance_to_image_plane"], + spawn=None, # camera already spawned in USD file + ), + ) + +The configuration supports customization of: + +* **Render Configuration**: Specify the GelSight sensor rendering parameters using predefined configs + (e.g., ``GELSIGHT_R15_CFG``, ``GELSIGHT_MINI_CFG`` from ``isaaclab_assets.sensors``) +* **Tactile Modalities**: + * ``enable_camera_tactile`` - Enable tactile RGB imaging through camera sensors + * ``enable_force_field`` - Enable force field computation and visualization +* **Force Field Grid**: Set tactile grid dimensions (``tactile_array_size``) and margins, which directly affects the spatial resolution of the computed force field +* **Contact Object Configuration**: Define properties of interacting objects using prim path expressions to locate objects with SDF collision meshes +* **Physics Parameters**: Control the sensor's force field computation: + * ``normal_contact_stiffness``, ``friction_coefficient``, ``tangential_stiffness`` - Normal stiffness, friction coefficient, and tangential stiffness +* **Camera Settings**: Configure resolution, update rates, and data types, currently only ``distance_to_image_plane`` (alias for ``depth``) is supported. + ``spawn`` is set to ``None`` by default, which means that the camera is already spawned in the USD file. + If you want to spawn the camera yourself and set focal length, etc., you can set the spawn configuration to a valid spawn configuration. + +Configuration Requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. important:: + The following requirements must be satisfied for proper sensor operation: + + **Camera Tactile Imaging** + If ``enable_camera_tactile=True``, a valid ``camera_cfg`` (TiledCameraCfg) must be provided with appropriate camera parameters. + + **Force Field Computation** + If ``enable_force_field=True``, the following parameters are required: + + * ``contact_object_prim_path_expr`` - Prim path expression to locate contact objects with SDF collision meshes + + **SDF Computation** + When force field computation is enabled, penalty-based normal and shear forces are computed using Signed Distance Field (SDF) queries. To achieve GPU acceleration: + + * Interacting objects should have SDF collision meshes + * An SDFView must be defined during initialization, therefore interacting objects should be specified before simulation. + + **Elastomer Configuration** + The sensor's ``prim_path`` must be configured as a child of the elastomer prim in the USD hierarchy. + The query points for the force field computation is computed from the surface of the elastomer mesh, which is searched for under the prim path of the elastomer. + + **Physics Materials** + The sensor uses physics materials to configure the compliant contact properties of the elastomer. + By default, physics material properties are pre-configured in the USD asset. However, you can override + these properties by specifying the following parameters in ``UsdFileWithCompliantContactCfg`` when + spawning the robot: + + * ``compliant_contact_stiffness`` - Contact stiffness for the elastomer surface + * ``compliant_contact_damping`` - Contact damping for the elastomer surface + * ``physics_material_prim_path`` - Prim path where physics material is applied (typically ``"elastomer"``) + + If any parameter is set to ``None``, the corresponding property from the USD asset will be retained. + + +Usage Example +~~~~~~~~~~~~~ + +To use the tactile sensor in a simulation environment, run the demo: + +.. code-block:: bash + + cd scripts/demos/sensors + python tacsl_sensor.py --use_tactile_rgb --use_tactile_ff --tactile_compliance_stiffness 100.0 --tactile_compliant_damping 1.0 --contact_object_type nut --num_envs 16 --save_viz --enable_cameras + +Available command-line options include: + +* ``--use_tactile_rgb``: Enable camera-based tactile sensing +* ``--use_tactile_ff``: Enable force field tactile sensing +* ``--contact_object_type``: Specify the type of contact object (nut, cube, etc.) +* ``--num_envs``: Number of parallel environments +* ``--save_viz``: Save visualization outputs for analysis +* ``--tactile_compliance_stiffness``: Override compliant contact stiffness (default: use USD asset values) +* ``--tactile_compliant_damping``: Override compliant contact damping (default: use USD asset values) +* ``--normal_contact_stiffness``: Normal contact stiffness for force field computation +* ``--tangential_stiffness``: Tangential stiffness for shear forces +* ``--friction_coefficient``: Friction coefficient for shear forces +* ``--debug_sdf_closest_pts``: Visualize closest SDF points for debugging +* ``--debug_tactile_sensor_pts``: Visualize tactile sensor points for debugging +* ``--trimesh_vis_tactile_points``: Enable trimesh-based visualization of tactile points + +For a complete list of available options: + +.. code-block:: bash + + python tacsl_sensor.py -h + +.. note:: + The demo examples are based on the Gelsight R1.5, which is a prototype sensor that is now discontinued. The same procedure can be adapted for other visuotactile sensors. + +.. figure:: ../../../_static/overview/sensors/tacsl_demo.jpg + :align: center + :figwidth: 100% + :alt: TacSL tactile sensor demo showing RGB tactile images and force field visualizations + +The tactile sensor supports multiple data modalities that provide comprehensive information about contact interactions: + + +Output Tactile Data +~~~~~~~~~~~~~~~~~~~ +**RGB Tactile Images** + Real-time generation of tactile RGB images as objects make contact with the sensor surface. These images show deformation patterns and contact geometry similar to gel-based tactile sensors [Si2022]_ + + +**Force Fields** + Detailed contact force field and pressure distributions across the sensor surface, including normal and shear components. + +.. list-table:: + :widths: 50 50 + :class: borderless + + * - .. figure:: ../../../_static/overview/sensors/tacsl_taxim_example.jpg + :align: center + :figwidth: 80% + :alt: Tactile output with RGB visualization + + - .. figure:: ../../../_static/overview/sensors/tacsl_force_field_example.jpg + :align: center + :figwidth: 80% + :alt: Tactile output with force field visualization + +Integration with Learning Frameworks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The tactile sensor is designed to integrate seamlessly with reinforcement learning and imitation learning frameworks. The structured tensor outputs can be directly used as observations in learning algorithms: + +.. code-block:: python + + def get_tactile_observations(self): + """Extract tactile observations for learning.""" + tactile_data = self.scene["tactile_sensor"].data + + # tactile RGB image + tactile_rgb = tactile_data.tactile_rgb_image + + # tactile depth image + tactile_depth = tactile_data.tactile_depth_image + + # force field + tactile_normal_force = tactile_data.tactile_normal_force + tactile_shear_force = tactile_data.tactile_shear_force + + return [tactile_rgb, tactile_depth, tactile_normal_force, tactile_shear_force] + + + +References +~~~~~~~~~~ + +.. [Akinola2025] Akinola, I., Xu, J., Carius, J., Fox, D., & Narang, Y. (2025). TacSL: A library for visuotactile sensor simulation and learning. *IEEE Transactions on Robotics*. +.. [Si2022] Si, Z., & Yuan, W. (2022). Taxim: An example-based simulation model for GelSight tactile sensors. *IEEE Robotics and Automation Letters*, 7(2), 2361-2368. diff --git a/docs/source/overview/developer-guide/repo_structure.rst b/docs/source/overview/developer-guide/repo_structure.rst index 22c5cb518208..a201886c0f8d 100644 --- a/docs/source/overview/developer-guide/repo_structure.rst +++ b/docs/source/overview/developer-guide/repo_structure.rst @@ -5,7 +5,6 @@ Repository organization IsaacLab ├── .vscode - ├── .flake8 ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE diff --git a/docs/source/overview/developer-guide/vs_code.rst b/docs/source/overview/developer-guide/vs_code.rst index 1b7190c341b2..a19889d1bdb8 100644 --- a/docs/source/overview/developer-guide/vs_code.rst +++ b/docs/source/overview/developer-guide/vs_code.rst @@ -75,3 +75,18 @@ and selecting ``Python: Select Interpreter``. For more information on how to set python interpreter for VSCode, please refer to the `VSCode documentation `_. + + +Setting up formatting and linting +--------------------------------- + +We use `ruff `_ as a formatter and linter. +These are configured in the ``.vscode/settings.json`` file: + +.. code-block:: json + + { + "ruff.configuration": "${workspaceFolder}/pyproject.toml", + } + +The ruff linter will show warnings and errors in your code to help you follow Python best practices and the project's coding standards. diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index 050dcd223534..c1cfb7dcb2c4 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -13,16 +13,23 @@ running the following command: .. tab-item:: :icon:`fa-brands fa-linux` Linux :sync: linux + .. note:: + Use ``--keyword `` (optional) to filter environments by keyword. + .. code:: bash - ./isaaclab.sh -p scripts/environments/list_envs.py + ./isaaclab.sh -p scripts/environments/list_envs.py --keyword .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows + .. note:: + Use ``--keyword `` (optional) to filter environments by keyword. + .. code:: batch - isaaclab.bat -p scripts\environments\list_envs.py + isaaclab.bat -p scripts\environments\list_envs.py --keyword + We are actively working on adding more environments to the list. If you have any environments that you would like to add to Isaac Lab, please feel free to open a pull request! @@ -163,6 +170,14 @@ for the lift-cube environment: +-------------------------+------------------------------+-----------------------------------------------------------------------------+ | |agibot_place_toy| | |agibot_place_toy-link| | Pick up and place an object in a box with a Agibot A2D humanoid robot | +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |reach_openarm_bi| | |reach_openarm_bi-link| | Move the end-effector to sampled target poses with the OpenArm robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |reach_openarm_uni| | |reach_openarm_uni-link| | Move the end-effector to a sampled target pose with the OpenArm robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |lift_openarm_uni| | |lift_openarm_uni-link| | Pick a cube and bring it to a sampled target position with the OpenArm robot| + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cabi_openarm_uni| | |cabi_openarm_uni-link| | Grasp the handle of a cabinet's drawer and open it with the OpenArm robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ .. |reach-franka| image:: ../_static/tasks/manipulation/franka_reach.jpg .. |reach-ur10| image:: ../_static/tasks/manipulation/ur10_reach.jpg @@ -183,6 +198,10 @@ for the lift-cube environment: .. |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_openarm_bi| image:: ../_static/tasks/manipulation/openarm_bi_reach.jpg +.. |reach_openarm_uni| image:: ../_static/tasks/manipulation/openarm_uni_reach.jpg +.. |lift_openarm_uni| image:: ../_static/tasks/manipulation/openarm_uni_lift.jpg +.. |cabi_openarm_uni| image:: ../_static/tasks/manipulation/openarm_uni_open_drawer.jpg .. |reach-franka-link| replace:: `Isaac-Reach-Franka-v0 `__ .. |reach-ur10-link| replace:: `Isaac-Reach-UR10-v0 `__ @@ -212,6 +231,10 @@ for the lift-cube environment: .. |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 `__ +.. |reach_openarm_bi-link| replace:: `Isaac-Reach-OpenArm-Bi-v0 `__ +.. |reach_openarm_uni-link| replace:: `Isaac-Reach-OpenArm-v0 `__ +.. |lift_openarm_uni-link| replace:: `Isaac-Lift-Cube-OpenArm-v0 `__ +.. |cabi_openarm_uni-link| replace:: `Isaac-Open-Drawer-OpenArm-v0 `__ Contact-rich Manipulation @@ -463,6 +486,28 @@ Navigation .. |anymal_c_nav| image:: ../_static/tasks/navigation/anymal_c_nav.jpg +Multirotor +~~~~~~~~~~ + +.. note:: + The multirotor entry provides an environment configuration for flying the ARL robot. + See the `drone_arl` folder and the ARL robot config + (`ARL_ROBOT_1_CFG`) in the codebase for details. + +.. |arl_robot_track_position_state_based-link| replace:: `Isaac-TrackPositionNoObstacles-ARL-Robot-1-v0 `__ + +.. |arl_robot_track_position_state_based| image:: ../_static/tasks/drone_arl/arl_robot_1_track_position_state_based.jpg + +.. table:: + :widths: 33 37 30 + + +----------------------------------------+---------------------------------------------+----------------------------------------------------------------------------------------+ + | World | Environment ID | Description | + +========================================+=============================================+========================================================================================+ + | |arl_robot_track_position_state_based| | |arl_robot_track_position_state_based-link| | Setpoint position control for the ARL robot using the track_position_state_based task. | + +----------------------------------------+---------------------------------------------+----------------------------------------------------------------------------------------+ + + Others ~~~~~~ @@ -1135,3 +1180,19 @@ inferencing, including reading from an already trained checkpoint and disabling - Isaac-Velocity-Rough-Unitree-Go2-Play-v0 - Manager Based - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Reach-OpenArm-Bi-v0 + - Isaac-Reach-OpenArm-Bi-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO) + * - Isaac-Reach-OpenArm-v0 + - Isaac-Reach-OpenArm-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO), **rl_games** (PPO) + * - Isaac-Lift-Cube-OpenArm-v0 + - Isaac-Lift-Cube-OpenArm-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO) + * - Isaac-Open-Drawer-OpenArm-v0 + - Isaac-Open-Drawer-OpenArm-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO) diff --git a/docs/source/overview/imitation-learning/teleop_imitation.rst b/docs/source/overview/imitation-learning/teleop_imitation.rst index d7d1b4d0a464..14017e65b5da 100644 --- a/docs/source/overview/imitation-learning/teleop_imitation.rst +++ b/docs/source/overview/imitation-learning/teleop_imitation.rst @@ -292,6 +292,15 @@ Using the Mimic generated data we can now train a state-based BC agent for ``Isa Visualizing results ^^^^^^^^^^^^^^^^^^^ +.. tip:: + + **Important: Testing Multiple Checkpoint Epochs** + + When evaluating policy performance, it is common for different training epochs to yield significantly different results. + If you don't see the expected performance, **always test policies from various epochs** (not just the final checkpoint) + to find the best-performing model. Model performance can vary substantially across training, and the final epoch + is not always optimal. + By inferencing using the generated model, we can visualize the results of the policy: .. tab-set:: @@ -315,6 +324,11 @@ By inferencing using the generated model, we can visualize the results of the po --device cpu --enable_cameras --task Isaac-Stack-Cube-Franka-IK-Rel-Visuomotor-v0 --num_rollouts 50 \ --checkpoint /PATH/TO/desired_model_checkpoint.pth +.. tip:: + + **If you don't see expected performance results:** Test policies from multiple checkpoint epochs, not just the final one. + Policy performance can vary significantly across training epochs, and intermediate checkpoints often outperform the final model. + .. note:: **Expected Success Rates and Timings for Franka Cube Stack Task** @@ -323,6 +337,7 @@ By inferencing using the generated model, we can visualize the results of the po * Data generation time: ~30 mins for state, ~4 hours for visuomotor (varies based on num envs the user runs) * BC RNN training time: 1000 epochs + ~30 mins (for state), 600 epochs + ~6 hours (for visuomotor) * BC RNN policy success rate: ~40-60% (for both state + visuomotor) + * **Recommendation:** Evaluate checkpoints from various epochs throughout training to identify the best-performing model Demo 1: Data Generation and Policy Training for a Humanoid Robot @@ -513,6 +528,11 @@ Visualize the results of the trained policy by running the following command, us .. note:: Change the ``NORM_FACTOR`` in the above command with the values generated in the training step. +.. tip:: + + **If you don't see expected performance results:** It is critical to test policies from various checkpoint epochs. + Performance can vary significantly between epochs, and the best-performing checkpoint is often not the final one. + .. figure:: https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/gr-1_steering_wheel_pick_place_policy.gif :width: 100% :align: center @@ -528,7 +548,7 @@ Visualize the results of the trained policy by running the following command, us * Success rate for data generation depends on the quality of human demonstrations (how well the user performs them) and dataset annotation quality. Both data generation and downstream policy success are sensitive to these factors and can show high variance. See :ref:`Common Pitfalls when Generating Data ` for tips to improve your dataset. * Data generation success for this task is typically 65-80% over 1000 demonstrations, taking 18-40 minutes depending on GPU hardware and success rate (19 minutes on a RTX ADA 6000 @ 80% success rate). * Behavior Cloning (BC) policy success is typically 75-86% (evaluated on 50 rollouts) when trained on 1000 generated demonstrations for 2000 epochs (default), depending on demonstration quality. Training takes approximately 29 minutes on a RTX ADA 6000. - * Recommendation: Train for 2000 epochs with 1000 generated demonstrations, and evaluate multiple checkpoints saved between the 1500th and 2000th epochs to select the best-performing policy. + * **Recommendation:** Train for 2000 epochs with 1000 generated demonstrations, and **evaluate multiple checkpoints saved between the 1000th and 2000th epochs** to select the best-performing policy. Testing various epochs is essential for finding optimal performance. Demo 2: Data Generation and Policy Training for Humanoid Robot Locomanipulation with Unitree G1 @@ -546,6 +566,16 @@ The robot picks up an object at the initial location (point A) and places it at :alt: G1 humanoid robot with locomanipulation performing a pick and place task :figclass: align-center +.. note:: + **Locomotion policy training** + + The locomotion policy used in this integration example was trained using the `AGILE `__ framework. + AGILE is an officially supported humanoid control training pipeline that leverages the manager based environment in Isaac Lab. It will also be + seamlessly integrated with other evaluation and deployment tools across Isaac products. This allows teams to rely on a single, maintained stack + covering all necessary infrastructure and tooling for policy training, with easy export to real-world deployment. The AGILE repository contains + updated pre-trained policies with separate upper and lower body policies for flexibtility. They have been verified in the real world and can be + directly deployed. Users can also train their own locomotion or whole-body control policies using the AGILE framework. + Generate the manipulation dataset ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -642,6 +672,11 @@ Visualize the trained policy performance: .. note:: Change the ``NORM_FACTOR`` in the above command with the values generated in the training step. +.. tip:: + + **If you don't see expected performance results:** Always test policies from various checkpoint epochs. + Different epochs can produce significantly different results, so evaluate multiple checkpoints to find the optimal model. + .. figure:: https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/locomanipulation-g-1_steering_wheel_pick_place.gif :width: 100% :align: center @@ -657,7 +692,7 @@ Visualize the trained policy performance: * Success rate for data generation depends on the quality of human demonstrations (how well the user performs them) and dataset annotation quality. Both data generation and downstream policy success are sensitive to these factors and can show high variance. See :ref:`Common Pitfalls when Generating Data ` for tips to improve your dataset. * Data generation success for this task is typically 65-82% over 1000 demonstrations, taking 18-40 minutes depending on GPU hardware and success rate (18 minutes on a RTX ADA 6000 @ 82% success rate). * Behavior Cloning (BC) policy success is typically 75-85% (evaluated on 50 rollouts) when trained on 1000 generated demonstrations for 2000 epochs (default), depending on demonstration quality. Training takes approximately 40 minutes on a RTX ADA 6000. - * Recommendation: Train for 2000 epochs with 1000 generated demonstrations, and evaluate multiple checkpoints saved between the 1500th and 2000th epochs to select the best-performing policy. + * **Recommendation:** Train for 2000 epochs with 1000 generated demonstrations, and **evaluate multiple checkpoints saved between the 1000th and 2000th epochs** to select the best-performing policy. Testing various epochs is essential for finding optimal performance. Generate the dataset with manipulation and point-to-point navigation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -851,6 +886,11 @@ Visualize the results of the trained policy by running the following command, us .. note:: Change the ``NORM_FACTOR`` in the above command with the values generated in the training step. +.. tip:: + + **If you don't see expected performance results:** Test policies from various checkpoint epochs, not just the final one. + Policy performance can vary substantially across training, and intermediate checkpoints often yield better results. + .. figure:: https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/gr-1_nut_pouring_policy.gif :width: 100% :align: center @@ -866,7 +906,7 @@ Visualize the results of the trained policy by running the following command, us * Success rate for data generation depends on the quality of human demonstrations (how well the user performs them) and dataset annotation quality. Both data generation and downstream policy success are sensitive to these factors and can show high variance. See :ref:`Common Pitfalls when Generating Data ` for tips to improve your dataset. * Data generation for 1000 demonstrations takes approximately 10 hours on a RTX ADA 6000. * Behavior Cloning (BC) policy success is typically 50-60% (evaluated on 50 rollouts) when trained on 1000 generated demonstrations for 600 epochs (default). Training takes approximately 15 hours on a RTX ADA 6000. - * Recommendation: Train for 600 epochs with 1000 generated demonstrations, and evaluate multiple checkpoints saved between the 300th and 600th epochs to select the best-performing policy. + * **Recommendation:** Train for 600 epochs with 1000 generated demonstrations, and **evaluate multiple checkpoints saved between the 300th and 600th epochs** to select the best-performing policy. Testing various epochs is critical for achieving optimal performance. .. _common-pitfalls-generating-data: diff --git a/docs/source/overview/reinforcement-learning/rl_frameworks.rst b/docs/source/overview/reinforcement-learning/rl_frameworks.rst index 34d47c17cdc5..5f9d25e06e05 100644 --- a/docs/source/overview/reinforcement-learning/rl_frameworks.rst +++ b/docs/source/overview/reinforcement-learning/rl_frameworks.rst @@ -71,18 +71,26 @@ Training Performance -------------------- We performed training with each RL library on the same ``Isaac-Humanoid-v0`` environment -with ``--headless`` on a single RTX PRO 6000 GPU using 4096 environments -and logged the total training time for 65.5M steps for each RL library. - +with ``--headless`` on a single NVIDIA GeForce RTX 4090 and logged the total training time +for 65.5M steps (4096 environments x 32 rollout steps x 500 iterations). +--------------------+-----------------+ | RL Library | Time in seconds | +====================+=================+ -| RL-Games | 207 | +| RL-Games | 201 | +--------------------+-----------------+ -| SKRL | 208 | +| SKRL | 201 | +--------------------+-----------------+ -| RSL RL | 199 | +| RSL RL | 198 | +--------------------+-----------------+ -| Stable-Baselines3 | 322 | +| Stable-Baselines3 | 287 | +--------------------+-----------------+ + +Training commands (check for the *'Training time: XXX seconds'* line in the terminal output): + +.. code:: bash + + python scripts/reinforcement_learning/rl_games/train.py --task Isaac-Humanoid-v0 --max_iterations 500 --headless + python scripts/reinforcement_learning/skrl/train.py --task Isaac-Humanoid-v0 --max_iterations 500 --headless + python scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Humanoid-v0 --max_iterations 500 --headless + python scripts/reinforcement_learning/sb3/train.py --task Isaac-Humanoid-v0 --max_iterations 500 --headless diff --git a/docs/source/overview/showroom.rst b/docs/source/overview/showroom.rst index de04650ab85e..bb2248375749 100644 --- a/docs/source/overview/showroom.rst +++ b/docs/source/overview/showroom.rst @@ -193,6 +193,30 @@ A few quick showroom scripts to run and checkout: :alt: Multiple assets managed through the same simulation handles +- Use the RigidObjectCollection spawn and view manipulation to demonstrate bin-packing example: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p scripts/demos/bin_packing.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p scripts\demos\bin_packing.py + + .. image:: ../_static/demos/bin_packing.jpg + :width: 100% + :alt: Spawning random number of random asset per env_id using combination of MultiAssetSpawner and RigidObjectCollection + + - Use the interactive scene and spawn a simple parallel robot for pick and place: @@ -228,6 +252,41 @@ A few quick showroom scripts to run and checkout: +- Teleoperate a Franka Panda robot using Haply haptic device with force feedback: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p scripts/demos/haply_teleoperation.py --websocket_uri ws://localhost:10001 --pos_sensitivity 1.65 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p scripts\demos\haply_teleoperation.py --websocket_uri ws://localhost:10001 --pos_sensitivity 1.65 + + .. image:: ../_static/demos/haply_teleop_franka.jpg + :width: 100% + :alt: Haply teleoperation with force feedback + + This demo requires Haply Inverse3 and VerseGrip devices. + The goal of this demo is to pick up the cube or touch it with the end-effector. + The Haply devices provide: + + * 3 dimensional position tracking for end-effector control + * Directional force feedback for contact sensing + * Button inputs for gripper and end-effector rotation control + + See :ref:`haply-teleoperation` for detailed setup instructions. + + + - Create and spawn procedurally generated terrains with different configurations: .. tab-set:: @@ -299,3 +358,27 @@ A few quick showroom scripts to run and checkout: .. image:: ../_static/demos/quadrupeds.jpg :width: 100% :alt: Quadrupeds in Isaac Lab + + +- Spawn a multi-mesh ray caster that uses Warp kernels for raycasting + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type objects + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p scripts\demos\sensors\multi_mesh_raycaster.py --num_envs 16 --asset_type objects + + .. image:: ../_static/demos/multi-mesh-raycast.jpg + :width: 100% + :alt: Multi-mesh raycaster in Isaac Lab diff --git a/docs/source/policy_deployment/02_gear_assembly/gear_assembly_policy.rst b/docs/source/policy_deployment/02_gear_assembly/gear_assembly_policy.rst new file mode 100644 index 000000000000..885b7fb6733a --- /dev/null +++ b/docs/source/policy_deployment/02_gear_assembly/gear_assembly_policy.rst @@ -0,0 +1,605 @@ +.. _walkthrough_sim_to_real: + +Training a Gear Insertion Policy and ROS Deployment +==================================================== + +This tutorial walks you through how to train a gear insertion assembly reinforcement learning (RL) policy that transfers from simulation to a real robot. The workflow consists of two main stages: + +1. **Simulation Training in Isaac Lab**: Train the policy in a high-fidelity physics simulation with domain randomization +2. **Real Robot Deployment with Isaac ROS**: Deploy the trained policy on real hardware using Isaac ROS and a custom ROS inference node + +This walkthrough covers the key principles and best practices for sim-to-real transfer using Isaac Lab, illustrated with a real-world example: + +- the Gear Assembly task for the UR10e robot with the Robotiq 2F-140 gripper or 2F-85 gripper + +**Task Details:** + +The gear assembly policy operates as follows: + +1. **Initial State**: The policy assumes the gear is already grasped by the gripper at the start of the episode +2. **Input Observations**: The policy receives the pose of the gear shaft (position and orientation) in which the gear should be inserted, obtained from a separate perception pipeline +3. **Policy Output**: The policy outputs delta joint positions (incremental changes to joint angles) to control the robot arm and perform the insertion +4. **Generalization**: The trained policy generalizes across 3 different gear sizes without requiring retraining for each size + + +.. figure:: ../../_static/policy_deployment/02_gear_assembly/gear_assembly_sim_real.webm + :align: center + :figwidth: 100% + :alt: Comparison of gear assembly in simulation versus real hardware + + Sim-to-real transfer: Gear assembly policy trained in Isaac Lab (left) successfully deployed on real UR10e robot (right). + +This environment has been successfully deployed on real UR10e robots without an IsaacLab dependency. + +**Scope of This Tutorial:** + +This tutorial focuses exclusively on the **training part** of the sim-to-real transfer workflow in Isaac Lab. For the complete deployment workflow on the real robot, including the exact steps to set up the vision pipeline, robot interface and the ROS inference node to run your trained policy on real hardware, please refer to the `Isaac ROS Documentation `_. + +Overview +-------- + +Successful sim-to-real transfer requires addressing three fundamental aspects: + +1. **Input Consistency**: Ensuring the observations your policy receives in simulation match those available on the real robot +2. **System Response Consistency**: Ensuring the robot and environment respond to actions in simulation the same way they do in reality +3. **Output Consistency**: Ensuring any post-processing applied to policy outputs in Isaac Lab is also applied during real-world inference + +When all three aspects are properly addressed, policies trained purely in simulation can achieve robust performance on real hardware without any real-world training data. + +**Debugging Tip**: When your policy fails on the real robot, the best approach to debug is to set up the real robot with the same initial observations as in simulation, then compare how the controller/system respond. This isolates whether the problem is from observation mismatch (Input Consistency) or physics/controller mismatch (System Response Consistency). + +Part 1: Input Consistency +-------------------------- + +The observations your policy receives must be consistent between simulation and reality. This means: + +1. The observation space should only include information available from real sensors +2. Sensor noise and delays should be modeled appropriately + +Using Real-Robot-Available Observations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Your simulation environment should only use observations that are available on the real robot and not use "privileged" information that wouldn't be available in deployment. + + +Observation Specification: Isaac-Deploy-GearAssembly-UR10e-2F140-v0 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Gear Assembly environment uses both proprioceptive and exteroceptive (vision) observations: + +.. list-table:: Gear Assembly Environment Observations + :widths: 25 25 25 25 + :header-rows: 1 + + * - Observation + - Dimension + - Real-World Source + - Noise in Training + * - ``joint_pos`` + - 6 (UR10e arm joints) + - UR10e controller + - None (proprioceptive) + * - ``joint_vel`` + - 6 (UR10e arm joints) + - UR10e controller + - None (proprioceptive) + * - ``gear_shaft_pos`` + - 3 (x, y, z position) + - FoundationPose + RealSense depth + - ±0.005 m (5mm, estimated error from FoundationPose + RealSense depth pipeline) + * - ``gear_shaft_quat`` + - 4 (quaternion orientation) + - FoundationPose + RealSense depth + - ±0.01 per component (~5° angular error, estimated error from FoundationPose + RealSense depth pipeline) + +**Implementation:** + +.. code-block:: python + + from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + # Robot joint states - NO noise for proprioceptive observations + joint_pos = ObsTerm( + func=mdp.joint_pos, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder_pan_joint", ...])}, + ) + + joint_vel = ObsTerm( + func=mdp.joint_vel, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder_pan_joint", ...])}, + ) + + # Gear shaft pose from FoundationPose perception + # ADD noise for exteroceptive (vision-based) observations + # Calibrated to match FoundationPose + RealSense D435 error + # Typical error: 3-8mm position, 3-7° orientation + gear_shaft_pos = ObsTerm( + func=mdp.gear_shaft_pos_w, + params={"asset_cfg": SceneEntityCfg("factory_gear_base")}, + noise=Unoise(n_min=-0.005, n_max=0.005), # ±5mm + ) + + # Quaternion noise: small uniform noise on each component + # Results in ~5° orientation error + gear_shaft_quat = ObsTerm( + func=mdp.gear_shaft_quat_w, + params={"asset_cfg": SceneEntityCfg("factory_gear_base")}, + noise=Unoise(n_min=-0.01, n_max=0.01), + ) + + def __post_init__(self): + self.enable_corruption = True # Enable for perception observations only + self.concatenate_terms = True + +**Why No Noise for Proprioceptive Observations?** + +Empirically, we found that policies trained without noise on proprioceptive observations (joint positions and velocities) transfer well to real hardware. The UR10e controller provides sufficiently accurate joint state feedback that modeling sensor noise doesn't improve sim-to-real transfer for these tasks. + + +Part 2: System Response Consistency +------------------------------------ + +Once your observations are consistent, you need to ensure the simulated robot and environment respond to actions the same way the real system does. In this use case, this involves three main aspects: + +1. Physics simulation parameters (friction, contact properties) +2. Actuator modeling (PD controller gains, effort limits) +3. Domain randomization + +Physics Parameter Tuning +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Accurate physics simulation is critical for contact-rich tasks. Key parameters include: + +- Friction coefficients (static and dynamic) +- Contact solver parameters +- Material properties +- Rigid body properties + +**Example: Gear Assembly Physics Configuration** + +The Gear Assembly task requires accurate contact modeling for insertion. Here's how friction is configured: + +.. code-block:: python + + # From joint_pos_env_cfg.py in Isaac-Deploy-GearAssembly-UR10e-2F140-v0 + + @configclass + class EventCfg: + """Configuration for events including physics randomization.""" + + # Randomize friction for gear objects + small_gear_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("factory_gear_small", body_names=".*"), + "static_friction_range": (0.75, 0.75), # Calibrated to real gear material + "dynamic_friction_range": (0.75, 0.75), + "restitution_range": (0.0, 0.0), # No bounce + "num_buckets": 16, + }, + ) + + # Similar configuration for gripper fingers + robot_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*finger"), + "static_friction_range": (0.75, 0.75), # Calibrated to real gripper + "dynamic_friction_range": (0.75, 0.75), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + +These friction values (0.75) were determined through iterative visual comparison: + +1. Record videos of the gear being grasped and manipulated on real hardware +2. Start training in simulation and observe the live simulation viewer +3. Look for physics issues (penetration, unrealistic slipping, poor contact) +4. Adjust friction coefficients and solver parameters and retry +5. Compare the gear's behavior in the gripper visually between sim and real +6. Repeat adjustments until behavior matches (no need to wait for full policy training) +7. Once physics looks good, train in headless mode with video recording: + + .. code-block:: bash + + python scripts/reinforcement_learning/rsl_rl/train.py \ + --task Isaac-Deploy-GearAssembly-UR10e-2F140-v0 \ + --headless \ + --video --video_length 800 --video_interval 5000 + +8. Review the recorded videos and compare with real hardware videos to verify physics behavior + +**Contact Solver Configuration** + +Contact-rich manipulation requires careful solver tuning. These parameters were calibrated through the same iterative visual comparison process as the friction coefficients: + +.. code-block:: python + + # Robot rigid body properties + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, # Robot is mounted, no gravity + max_depenetration_velocity=5.0, # Control interpenetration resolution + linear_damping=0.0, # No artificial damping + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, # Important for accurate dynamics + solver_position_iteration_count=4, # Balance accuracy vs performance + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, # Allow large contact forces + ), + +**Important**: The ``solver_position_iteration_count`` is a critical parameter for contact-rich tasks. Increasing this value improves collision simulation stability and reduces penetration issues, but it also increases simulation and training time. For the gear assembly task, we use ``solver_position_iteration_count=4`` as a balance between physics accuracy and computational performance. If you observe penetration or unstable contacts, try increasing to 8 or 16, but expect slower training. + +.. code-block:: python + + # Articulation properties + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=4, + solver_velocity_iteration_count=1, + ), + + # Contact properties + collision_props=sim_utils.CollisionPropertiesCfg( + contact_offset=0.005, # 5mm contact detection distance + rest_offset=0.0, # Objects touch at 0 distance + ), + +Actuator Modeling +~~~~~~~~~~~~~~~~~ + +Accurate actuator modeling ensures the simulated robot moves like the real one. This includes: + +- PD controller gains (stiffness and damping) +- Effort and velocity limits +- Joint friction + +**Controller Choice: Impedance Control** + +For the UR10e deployment, we use an impedance controller interface. Using a simpler controller like impedance control reduces the chances of variation between simulation and reality compared to more complex controllers (e.g., operational space control, hybrid force-position control). Simpler controllers: + +- Have fewer parameters that can mismatch between sim and real +- Are easier to model accurately in simulation +- Have more predictable behavior that's easier to replicate +- Reduce the controller complexity as a source of sim-real gap + +**Example: UR10e Actuator Configuration** + +.. code-block:: python + + # Default UR10e actuator configuration + actuators = { + "arm": ImplicitActuatorCfg( + joint_names_expr=["shoulder_pan_joint", "shoulder_lift_joint", + "elbow_joint", "wrist_1_joint", "wrist_2_joint", "wrist_3_joint"], + effort_limit=87.0, # From UR10e specifications + velocity_limit=2.0, # From UR10e specifications + stiffness=800.0, # Calibrated to match real behavior + damping=40.0, # Calibrated to match real behavior + ), + } + +**Domain Randomization of Actuator Parameters** + +To account for variations in real robot behavior, randomize actuator gains during training: + +.. code-block:: python + + # From EventCfg in the Gear Assembly environment + robot_joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder_.*", "elbow_.*", "wrist_.*"]), + "stiffness_distribution_params": (0.75, 1.5), # 75% to 150% of nominal + "damping_distribution_params": (0.3, 3.0), # 30% to 300% of nominal + "operation": "scale", + "distribution": "log_uniform", + }, + ) + + +**Joint Friction Randomization** + +Real robots have friction in their joints that varies with position, velocity, and temperature. For the UR10e with impedance controller interface, we observed significant stiction (static friction) causing the controller to not reach target joint positions. + +**Characterizing Real Robot Behavior:** + +To quantify this behavior, we plotted the step response of the impedance controller on the real robot and observed contact offsets of approximately 0.25 degrees from the commanded setpoint. This steady-state error is caused by joint friction opposing the controller's commanded motion. Based on these measurements, we added joint friction modeling in simulation to replicate this behavior: + +.. code-block:: python + + joint_friction = EventTerm( + func=mdp.randomize_joint_parameters, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder_.*", "elbow_.*", "wrist_.*"]), + "friction_distribution_params": (0.3, 0.7), # Add 0.3 to 0.7 Nm friction + "operation": "add", + "distribution": "uniform", + }, + ) + +**Why Joint Friction Matters**: Without modeling joint friction in simulation, the policy learns to expect that commanded joint positions are always reached. On the real robot, stiction prevents small movements and causes steady-state errors. By adding friction during training, the policy learns to account for these effects and commands appropriately larger motions to overcome friction. + +**Compensating for Stiction with Action Scaling:** + +To help the policy overcome stiction on the real robot, we also increased the output action scaling. The Isaac ROS documentation notes that a higher action scale (0.0325 vs 0.025) is needed to overcome the higher static friction (stiction) compared to the 2F-85 gripper. This increased scaling ensures the policy commands are large enough to overcome the friction forces observed in the step response analysis. + +Action Space Design +~~~~~~~~~~~~~~~~~~~ + +Your action space should match what the real robot controller can execute. For this task we found that **incremental joint position control** is the most reliable approach. + +**Example: Gear Assembly Action Configuration** + +.. code-block:: python + + # For contact-rich manipulation, smaller action scale for more precise control + self.joint_action_scale = 0.025 # ±2.5 degrees per step + + self.actions.arm_action = mdp.RelativeJointPositionActionCfg( + asset_name="robot", + joint_names=["shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint", + "wrist_1_joint", "wrist_2_joint", "wrist_3_joint"], + scale=self.joint_action_scale, + use_zero_offset=True, + ) + +The action scale is a critical hyperparameter that should be tuned based on: + +- Task precision requirements (smaller for contact-rich tasks) +- Control frequency (higher frequency allows larger steps) + +Domain Randomization Strategy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Domain randomization should cover the range of conditions in which you want the real robot to perform. Increasing randomization ranges makes it harder for the policy to learn, but allows for larger variations in inputs and system parameters. The key is to balance training difficulty with robustness: randomize enough to cover real-world variations, but not so much that the policy cannot learn effectively. + +**Pose Randomization** + +For manipulation tasks, randomize object poses to ensure the policy works across the workspace: + +.. code-block:: python + + # From Gear Assembly environment + randomize_gears_and_base_pose = EventTerm( + func=gear_assembly_events.randomize_gears_and_base_pose, + mode="reset", + params={ + "pose_range": { + "x": [-0.1, 0.1], # ±10cm + "y": [-0.25, 0.25], # ±25cm + "z": [-0.1, 0.1], # ±10cm + "roll": [-math.pi/90, math.pi/90], # ±2 degrees + "pitch": [-math.pi/90, math.pi/90], # ±2 degrees + "yaw": [-math.pi/6, math.pi/6], # ±30 degrees + }, + "gear_pos_range": { + "x": [-0.02, 0.02], # ±2cm relative to base + "y": [-0.02, 0.02], + "z": [0.0575, 0.0775], # 5.75-7.75cm above base + }, + "rot_randomization_range": { + "roll": [-math.pi/36, math.pi/36], # ±5 degrees + "pitch": [-math.pi/36, math.pi/36], + "yaw": [-math.pi/36, math.pi/36], + }, + }, + ) + +**Initial State Randomization** + +Randomizing the robot's initial configuration helps the policy handle different starting conditions: + +.. code-block:: python + + set_robot_to_grasp_pose = EventTerm( + func=gear_assembly_events.set_robot_to_grasp_pose, + mode="reset", + params={ + "robot_asset_cfg": SceneEntityCfg("robot"), + "rot_offset": [0.0, math.sqrt(2)/2, math.sqrt(2)/2, 0.0], # Base gripper orientation + "pos_randomization_range": { + "x": [-0.0, 0.0], + "y": [-0.005, 0.005], # ±5mm variation + "z": [-0.003, 0.003], # ±3mm variation + }, + "gripper_type": "2f_140", + }, + ) + +Part 3: Training the Policy in Isaac Lab +----------------------------------------- + +Now that we've covered the key principles for sim-to-real transfer, let's train the gear assembly policy in Isaac Lab. + +Step 1: Visualize the Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, launch the training with a small number of environments and visualization enabled to verify that the environment is set up correctly: + +.. code-block:: bash + + # Launch training with visualization + python scripts/reinforcement_learning/rsl_rl/train.py \ + --task Isaac-Deploy-GearAssembly-UR10e-2F140-v0 \ + --num_envs 4 + +.. note:: + + For the Robotiq 2F-85 gripper, use ``--task Isaac-Deploy-GearAssembly-UR10e-2F85-v0`` instead. + +This will open the Isaac Sim viewer where you can observe the training process in real-time. + +.. figure:: ../../_static/policy_deployment/02_gear_assembly/sim_real_gear_assembly_train.jpg + :align: center + :figwidth: 100% + :alt: Gear assembly training visualization in Isaac Lab + + Training visualization showing multiple parallel environments with robots grasping gears. + +**What to Expect:** + +In the early stages of training, you should see the robots moving around with the gears grasped by the grippers, but they won't be successfully inserting the gears yet. This is expected behavior as the policy is still learning. The robots will move the grasped gear in various directions. Once you've verified the environment looks correct, stop the training (Ctrl+C) and proceed to full-scale training. + +Step 2: Full-Scale Training with Video Recording +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now launch the full training run with more parallel environments in headless mode for faster training. We'll also enable video recording to monitor progress: + +.. code-block:: bash + + # Full training with video recording + python scripts/reinforcement_learning/rsl_rl/train.py \ + --task Isaac-Deploy-GearAssembly-UR10e-2F140-v0 \ + --headless \ + --num_envs 256 \ + --video --video_length 800 --video_interval 5000 + +This command will: + +- Run 256 parallel environments for efficient training +- Run in headless mode (no visualization) for maximum performance +- Record videos every 5000 steps to monitor training progress +- Save videos with 800 frames each + +Training typically takes ~12-24 hours for a robust insertion policy. The videos will be saved in the ``logs`` directory and can be reviewed to assess policy performance during training. + +.. note:: + + **GPU Memory Considerations**: The default configuration uses 256 parallel environments, which should work on most modern GPUs (e.g., RTX 3090, RTX 4090, A100). For better sim-to-real transfer performance, you can increase ``solver_position_iteration_count`` from 4 to 196 in ``gear_assembly_env_cfg.py`` and ``joint_pos_env_cfg.py`` for more realistic contact simulation, but this requires a larger GPU (e.g., RTX PRO 6000 with 40GB+ VRAM). Higher solver iteration counts reduce penetration and improve contact stability but significantly increase GPU memory usage. + + +**Monitoring Training Progress with TensorBoard:** + +You can monitor training metrics in real-time using TensorBoard. Open a new terminal and run: + +.. code-block:: bash + + ./isaaclab.sh -p -m tensorboard.main --logdir + +Replace ```` with the path to your training logs (e.g., ``logs/rsl_rl/gear_assembly_ur10e/2025-11-19_19-31-01``). TensorBoard will display plots showing rewards, episode lengths, and other metrics. Verify that the rewards are increasing over iterations to ensure the policy is learning successfully. + + +Step 3: Deploy on Real Robot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once training is complete, follow the `Isaac ROS inference documentation `_ to deploy your policy. + +The Isaac ROS deployment pipeline directly uses the trained model checkpoint (``.pt`` file) along with the ``agent.yaml`` and ``env.yaml`` configuration files generated during training. No additional export step is required. + +The deployment pipeline uses Isaac ROS and a custom ROS inference node to run the policy on real hardware. The pipeline includes: + +1. **Perception**: Camera-based pose estimation (FoundationPose, Segment Anything) +2. **Motion Planning**: cuMotion for collision-free trajectories +3. **Policy Inference**: Your trained policy running at control frequency in a custom ROS inference node +4. **Robot Control**: Low-level controller executing commands + + +Troubleshooting +--------------- + +This section covers common errors you may encounter during training and their solutions. + +PhysX Collision Stack Overflow +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Error Message:** + +.. code-block:: text + + PhysX error: PxGpuDynamicsMemoryConfig::collisionStackSize buffer overflow detected, + please increase its size to at least 269452544 in the scene desc! + Contacts have been dropped. + +**Cause:** This error occurs when the GPU collision detection buffer is too small for the number of contacts being simulated. This is common in contact-rich environments like gear assembly. + +**Solution:** Increase the ``gpu_collision_stack_size`` parameter in ``gear_assembly_env_cfg.py``: + +.. code-block:: python + + # In GearAssemblyEnvCfg class + sim: SimulationCfg = SimulationCfg( + physx=PhysxCfg( + gpu_collision_stack_size=2**31, # Increase this value if you see overflow errors + gpu_max_rigid_contact_count=2**23, + gpu_max_rigid_patch_count=2**23, + ), + ) + +The error message will suggest a minimum size. Set ``gpu_collision_stack_size`` to at least the recommended value (e.g., if the error says "at least 269452544", set it to ``2**28`` or ``2**29``). Note that increasing this value increases GPU memory usage. + +CUDA Out of Memory +~~~~~~~~~~~~~~~~~~ + +**Error Message:** + +.. code-block:: text + + torch.OutOfMemoryError: CUDA out of memory. + +**Cause:** The GPU does not have enough memory to run the requested number of parallel environments with the current simulation parameters. + +**Solutions (in order of preference):** + +1. **Reduce the number of parallel environments:** + + .. code-block:: bash + + python scripts/reinforcement_learning/rsl_rl/train.py \ + --task Isaac-Deploy-GearAssembly-UR10e-2F140-v0 \ + --headless \ + --num_envs 128 # Reduce from 256 to 128, 64, etc. + + **Trade-off:** Using fewer environments will reduce sample diversity per training iteration and may slow down training convergence. You may need to train for more iterations to achieve the same performance. However, the final policy quality should be similar. + +2. **If using increased solver iteration counts** (values higher than the default 4): + + In both ``gear_assembly_env_cfg.py`` and ``joint_pos_env_cfg.py``, reduce ``solver_position_iteration_count`` back to the default value of 4, or use intermediate values like 8 or 16: + + .. code-block:: python + + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, # Use default value + # ... other parameters + ), + + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + solver_position_iteration_count=4, # Use default value + # ... other parameters + ), + + **Trade-off:** Lower solver iteration counts may result in less realistic contact dynamics and more penetration issues. The default value of 4 provides a good balance for most use cases. + +3. **Disable video recording during training:** + + Remove the ``--video`` flags to save GPU memory: + + .. code-block:: bash + + python scripts/reinforcement_learning/rsl_rl/train.py \ + --task Isaac-Deploy-GearAssembly-UR10e-2F140-v0 \ + --headless \ + --num_envs 256 + + You can always evaluate the trained policy later with visualization. + + +Further Resources +----------------- + +- `IndustReal: Transferring Contact-Rich Assembly Tasks from Simulation to Reality `_ +- `FORGE: Force-Guided Exploration for Robust Contact-Rich Manipulation under Uncertainty `_ +- Sim-to-Real Policy Transfer for Whole Body Controllers: :ref:`sim2real` - Shows how to train and deploy a whole body controller for legged robots using Isaac Lab with the Newton backend +- `Isaac ROS Manipulation Documentation `_ +- `Isaac ROS Gear Assembly Tutorial `_ +- RL Training Tutorial: :ref:`tutorial-run-rl-training` diff --git a/docs/source/policy_deployment/index.rst b/docs/source/policy_deployment/index.rst index 72a668c00933..3ee100f22174 100644 --- a/docs/source/policy_deployment/index.rst +++ b/docs/source/policy_deployment/index.rst @@ -10,3 +10,4 @@ Below, you’ll find detailed examples of various policies for training and depl 00_hover/hover_policy 01_io_descriptors/io_descriptors_101 + 02_gear_assembly/gear_assembly_policy diff --git a/docs/source/refs/contributing.rst b/docs/source/refs/contributing.rst index bc2d60c426a5..411742fd19e8 100644 --- a/docs/source/refs/contributing.rst +++ b/docs/source/refs/contributing.rst @@ -515,9 +515,7 @@ Tools We use the following tools for maintaining code quality: * `pre-commit `__: Runs a list of formatters and linters over the codebase. -* `black `__: The uncompromising code formatter. -* `flake8 `__: A wrapper around PyFlakes, pycodestyle and - McCabe complexity checker. +* `ruff `__: An extremely fast Python linter and formatter. Please check `here `__ for instructions to set these up. To run over the entire repository, please execute the diff --git a/docs/source/refs/release_notes.rst b/docs/source/refs/release_notes.rst index 57d5e1891cc9..8ba703ddcde2 100644 --- a/docs/source/refs/release_notes.rst +++ b/docs/source/refs/release_notes.rst @@ -4,6 +4,352 @@ Release Notes The release notes are now available in the `Isaac Lab GitHub repository `_. We summarize the release notes here for convenience. +v2.3.2 +====== + +What's Changed +-------------- + +This release focuses on stability, infrastructure improvements, workflow refinements, and incremental feature expansions, along with some significant new features, including **Multirotor and thruster support for drones**, **Multi-mesh RayCaster**, **Visual-based tactile sensor**, **Haply device integration**, and new **OpenArm environments**. It includes improvements to training workflows, teleoperation and Mimic pipelines, Ray integration, simulation utilities, and developer tooling, along with a large number of robustness and quality-of-life fixes. + +This will be our final release on the current **main** branch as we shift our development focus towards the **develop** branch. We anticipate large restructuring changes to happen on **develop**. While we hope to continue taking in contributions from the community, we will focus more time on our development towards Isaac Lab 3.0. For existing PRs, please re-target the target branch to **develop** to stay up-to-date with the latest changes. + +New Features +------------ + +Core & Simulation +~~~~~~~~~~~~~~~~~ + +* Adds Raycaster with tracking support for dynamic meshes by @renezurbruegg in https://github.com/isaac-sim/IsaacLab/pull/3298 +* Adds visual-based tactile sensor with shape sensing example by @JuanaDd in https://github.com/isaac-sim/IsaacLab/pull/3420 +* Adds wrench composers allowing the composition of multiple wrenches on the same bodies by @AntoineRichard in https://github.com/isaac-sim/IsaacLab/pull/3287 +* Adds multirotor/thruster actuator, multirotor asset and manager-based ARL drone task https://github.com/isaac-sim/IsaacLab/pull/3760 by @mihirk284 @grzemal @Zwoelf12 +* Adds automatic transform discovery for IMU sensors to find valid parent bodies by @bmccann-bdai in https://github.com/isaac-sim/IsaacLab/pull/3864 +* Adds friction force reporting to ContactSensor by @gattra-rai in https://github.com/isaac-sim/IsaacLab/pull/3563 +* Adds MJCF spawner for importing MJCF-based assets by @KyleM73 in https://github.com/isaac-sim/IsaacLab/pull/1672 + +Learning & Environments +~~~~~~~~~~~~~~~~~~~~~~~ + +* Adds OpenArm environments by @JinnnK in https://github.com/isaac-sim/IsaacLab/pull/4089 + +Mimic & Teleoperation +~~~~~~~~~~~~~~~~~~~~~ + +* Adds Haply device API with force feedback and teleoperation demo by @mingxueg-nv in https://github.com/isaac-sim/IsaacLab/pull/3873 +* Refactors retargeters and adds Quest retargeters for G1 tasks by @rwiltz in https://github.com/isaac-sim/IsaacLab/pull/3950 +* Adds Arena G1 locomanipulation retargeters by @rwiltz in https://github.com/isaac-sim/IsaacLab/pull/4140 +* Adds APIs to Isaac Lab Mimic for loco-manipulation data generation by @peterd-NV in https://github.com/isaac-sim/IsaacLab/pull/3992 + +Improvements +------------ + +Core & Simulation +~~~~~~~~~~~~~~~~~ + +* Adds preserve-order flag to JointPositionToLimitsAction by @renezurbruegg in https://github.com/isaac-sim/IsaacLab/pull/3716 +* Adds parsing of instanced meshes to prim fetching utilities by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/3367 +* Adds configurable logdir parameter to environments by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/3391 +* Exposes PhysX flag solveArticulationContactLast via PhysxCfg by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/3502 +* Removes pickle dependency for config load and dump by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/3709 +* Improves recorder manager to support custom demo indices by @rebeccazhang0707 in https://github.com/isaac-sim/IsaacLab/pull/3552 +* Normalizes Python logging by replacing remaining omni.log usage by @pascal-roth in https://github.com/isaac-sim/IsaacLab/pull/3912 +* Replaces Isaac Sim stage_utils, prim_utils, and nucleus_utils with Isaac Lab implementations by @pascal-roth in https://github.com/isaac-sim/IsaacLab/pull/3921, https://github.com/isaac-sim/IsaacLab/pull/3923, https://github.com/isaac-sim/IsaacLab/pull/3924 +* Breaks actuator configuration into multiple files to avoid circular imports by @bmccann-bdai in https://github.com/isaac-sim/IsaacLab/pull/3994 +* Moves logging configuration into shared utilities by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4298 +* Caches Isaac Sim package version for faster lookup by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4299 +* Simplifies imports of stage and prim utilities by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4286 +* Randomizes viscous and dynamic joint friction consistent with Isaac Sim 5.0 by @GiulioRomualdi in https://github.com/isaac-sim/IsaacLab/pull/3318 +* Prevents randomization of rigid body mass to zero or negative values by @jtigue-bdai in https://github.com/isaac-sim/IsaacLab/pull/4060 +* Improves image plotting normalization and colorization by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4302 +* Adds Fabric backend support to isaaclab.sim.views.XformPrimView by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/4374 + +Learning & Environments +~~~~~~~~~~~~~~~~~~~~~~~ + +* Enhances PBT usability with small workflow improvements by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/3449 +* Supports vectorized environments for pick-and-place demo by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/3996 +* Registers direct environments to Gymnasium using string-style imports by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/3803 +* Updates Gymnasium dependency to version 1.2.1 by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/3696 +* Updates SB3 PPO configuration to reduce excessive training time by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/3726 +* Adds support for validating replay success using task termination conditions by @yami007007 in https://github.com/isaac-sim/IsaacLab/pull/4170 +* Adds early stopping support for Ray-based training by @ozhanozen in https://github.com/isaac-sim/IsaacLab/pull/3276 +* Adds support for custom ProgressReporter implementations in Ray integration by @ozhanozen in https://github.com/isaac-sim/IsaacLab/pull/3269 +* Updates rsl_rl to version 3.1.2 to support state-dependent standard deviation by @ashwinvkNV in https://github.com/isaac-sim/IsaacLab/pull/3867 + +Infrastructure +~~~~~~~~~~~~~~ + +* Switches linting and import sorting to Ruff by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4329, https://github.com/isaac-sim/IsaacLab/pull/4377 +* Moves flake8 and pytest configuration into pyproject.toml by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4335, https://github.com/isaac-sim/IsaacLab/pull/4376 +* Removes dependency on XformPrim for create_prim by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4307 +* Updates copyright year to 2026 by @ashwinvkNV in https://github.com/isaac-sim/IsaacLab/pull/4311 +* Restricts .gitignore dataset rule to top-level directory only by @louislelay in https://github.com/isaac-sim/IsaacLab/pull/3400 +* Adds uv as an alternative to conda in isaaclab.sh by @KyleM73 in https://github.com/isaac-sim/IsaacLab/pull/3172 +* Fixes transformers dependency for theia issue and failing tests by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/4484 + +Bug Fixes +--------- + +Core & Simulation +~~~~~~~~~~~~~~~~~ + +* Fixes missing actuator indices variable in joint randomization by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/3447 +* Fixes ViewportCameraController numpy array missing datatype by @T-K-233 in https://github.com/isaac-sim/IsaacLab/pull/3375 +* Fixes PDActuator docstring mismatch with implementation by @lorenwel in https://github.com/isaac-sim/IsaacLab/pull/3493 +* Fixes rail difficulty-based height computation in mesh terrains by @KyleM73 in https://github.com/isaac-sim/IsaacLab/pull/3254 +* Fixes contact threshold handling when activating contact sensors by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/3498 +* Fixes indexing errors in joint parameter randomization by @GiulioRomualdi in https://github.com/isaac-sim/IsaacLab/pull/4051 +* Fixes noisy velocities near joint limits by @AntoineRichard in https://github.com/isaac-sim/IsaacLab/pull/3989 +* Fixes mesh converter not setting collision approximation attributes by @Soappyooo in https://github.com/isaac-sim/IsaacLab/pull/4082 +* Fixes returned normal tensor shape in TiledCamera by @Rabbit-Hu in https://github.com/isaac-sim/IsaacLab/pull/4241 +* Fixes advanced indexing shape mismatch in JointPositionToLimitsAction by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/3865 +* Fixes teleoperation crash when using DirectRL environments by @emmanuel-ferdman in https://github.com/isaac-sim/IsaacLab/pull/4364 +* Fixes lidar pattern horizontal resolution bug by @pascal-roth in https://github.com/isaac-sim/IsaacLab/pull/4452 + +Learning & Environments +~~~~~~~~~~~~~~~~~~~~~~~ + +* Fixes CUDA version parsing for AutoMate environments by @yijieg in https://github.com/isaac-sim/IsaacLab/pull/3795 + +Infrastructure & Tooling +~~~~~~~~~~~~~~~~~~~~~~~~ + +* Fixes CI behavior to correctly fail fork PRs when general tests fail by @nv-apoddubny in https://github.com/isaac-sim/IsaacLab/pull/3412 +* Fixes docker availability check in isaaclab.sh on systems without Docker by @klakhi in https://github.com/isaac-sim/IsaacLab/pull/4180 +* Forces CRLF line endings for .bat files to avoid Windows execution errors by @jiang131072 in https://github.com/isaac-sim/IsaacLab/pull/3624 +* Fixes environment test failures and disables unstable tests by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/3413 +* Fixes vulnerability in eval usage for Ray resource parsing by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/4425 +* Fixes curobo dockerfile for CI runs by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/4462 + +Documentation +------------- + +* Improves contribution guidelines for Isaac Lab by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/3403 +* Abstracts common installation steps in documentation by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/3445 +* Updates SkillGen documentation with data generation commands and success rates by @njawale42 in https://github.com/isaac-sim/IsaacLab/pull/3702 +* Adds Newton Beta documentation updates and visualizer guidance by @kellyguo11 and @Milad-Rakhsha-NV in https://github.com/isaac-sim/IsaacLab/pull/3518, https://github.com/isaac-sim/IsaacLab/pull/3551 +* Adds automated checks for broken documentation links and fixes existing ones by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/3888 +* Updates technical report link for Isaac Lab by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4074 +* Adds clarification on missing pip in uv virtual environments by @DBinK in https://github.com/isaac-sim/IsaacLab/pull/4055 +* Adds keyword filtering documentation for list_envs.py by @louislelay in https://github.com/isaac-sim/IsaacLab/pull/3384 +* Adds documentation for Multirotor feature by @Mayankm96 in https://github.com/isaac-sim/IsaacLab/pull/4400 +* Adds documentation for PVD and OVD comparison by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/4409 + +Migration Guide +--------------- + +External Force and Torque Application - Wrench Composers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +The ``set_external_force_and_torque()`` method on articulations, rigid bodies, and rigid body collections has been deprecated in favor of a new composable wrench system. + +Related PR: https://github.com/isaac-sim/IsaacLab/pull/3287 + +**New Features:** +- **Permanent Wrench Composer**: Applies forces/torques that persist across simulation steps until explicitly changed +- **Instantaneous Wrench Composer**: Applies forces/torques for a single simulation step, then automatically resets +- **Composability**: Multiple forces and torques can now be added together on the same body +- **Mixed Frame Support**: Seamlessly compose local and global frame wrenches + +**Migration Guide:** + +**Old API (Deprecated):** + +.. code-block:: python + + # Old method - overwrites previous forces + asset.set_external_force_and_torque( + forces=torch.ones(1, 1, 3), + torques=torch.ones(1, 1, 3), + body_ids=[0], + env_ids=[0], + is_global=False, + ) + +**New API:** + +.. code-block:: python + + # Set initial permanent forces (replaces previous) + asset.permanent_wrench_composer.set_forces_and_torques( + forces=torch.ones(1, 1, 3), + env_ids=[0], + body_ids=[0], + ) + + # Compose additional forces on the same body + asset.permanent_wrench_composer.add_forces_and_torques( + forces=torch.ones(1, 1, 3), + env_ids=[0], + body_ids=[0], + is_global=True, # Mix local and global frames + ) + + # Add torques independently + asset.permanent_wrench_composer.add_forces_and_torques( + torques=torch.ones(1, 1, 3), + env_ids=[0], + body_ids=[0], + ) + + # Apply forces and torques together with custom application points + asset.permanent_wrench_composer.add_forces_and_torques( + forces=torch.ones(1, 1, 3), + torques=torch.ones(1, 1, 3), + positions=torch.ones(1, 1, 3), + env_ids=[0], + body_ids=[0], + ) + +**Instantaneous Wrenches (New):** + +.. code-block:: python + + # Apply forces for a single simulation step only + asset.instantaneous_wrench_composer.add_forces_and_torques( + forces=torch.ones(1, 1, 3), + env_ids=[0], + body_ids=[0], + ) + + # Multiple instantaneous wrenches compose automatically + asset.instantaneous_wrench_composer.add_forces_and_torques( + forces=torch.ones(1, 2, 3), # Add more forces + env_ids=[0], + body_ids=[0, 1], + ) + # These are automatically reset after write_data_to_sim() + +**Key Differences:** + +- ``set_forces_and_torques()`` replaces existing wrenches +- ``add_forces_and_torques()`` composes with existing wrenches +- Permanent and instantaneous wrenches compose automatically +- Instantaneous wrenches auto-clear after each simulation step + +**Use Cases:** +- **Drones**: Compose thrust forces with aerodynamic drag and wind disturbances +- **Boats**: Apply buoyancy forces with wave-induced motions + + +Formatting and Linting - Migration to Ruff +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The project has migrated from multiple tools (``flake8`` for linting, ``black`` for formatting, ``isort`` for import sorting) to a unified toolchain using ``ruff`` for all formatting and linting tasks. + +Related PRs: https://github.com/isaac-sim/IsaacLab/pull/4329, https://github.com/isaac-sim/IsaacLab/pull/4377, https://github.com/isaac-sim/IsaacLab/pull/4335, https://github.com/isaac-sim/IsaacLab/pull/4376 + + +**Why:** + +- Faster performance (10-100x speedup) +- Unified configuration in ``pyproject.toml`` +- More consistent formatting and linting rules +- Simplified developer workflow + +**Migration Steps:** + +1. **Update configuration files:** + + .. code-block:: bash + + # Copy the updated configuration from the main branch + # Files to update: pyproject.toml, .pre-commit-config.yaml + +2. **Apply new formatting:** + + .. code-block:: bash + + ./isaaclab.sh --format + +3. **Resolve merge conflicts:** + If you encounter merge conflicts after updating, they likely originate from formatting differences. After copying the new configuration files, rerun the formatting command and commit the changes. + +.. note:: + + Pre-commit hooks will automatically run ``ruff`` on staged files. Ensure your code is formatted + before committing to avoid CI failures. + + +USD Utilities - Unified ``isaaclab.sim.utils`` Module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Isaac Lab now provides its own comprehensive USD utility module (``isaaclab.sim.utils``) instead of relying on scattered utilities from Isaac Sim's ``isaacsim.core.utils`` packages. + +Related PR: https://github.com/isaac-sim/IsaacLab/pull/4286 + +**Why:** + +- **Better Organization**: All USD operations grouped into logical submodules (stage, prims, queries, transforms, semantics) +- **Type Hints**: Full type annotations for better IDE support and code safety +- **Version Compatibility**: Handles differences between Isaac Sim versions automatically + +**Old API (Isaac Sim utilities):** + +.. code-block:: python + + import isaac.core.utils.stage as stage_utils + import isaac.core.utils.prims as prim_utils + + # Stage operations + stage_utils.create_new_stage() + current_stage = stage_utils.get_current_stage() + + # Prim operations + prim_utils.create_prim("/World/Cube", "Cube") + prim_utils.delete_prim("/World/OldObject") + +**New API (Isaac Lab utilities):** + +.. code-block:: python + + import isaaclab.sim as sim_utils + + # Stage operations + sim_utils.create_new_stage() + current_stage = sim_utils.get_current_stage() + + # Prim operations + sim_utils.create_prim("/World/Cube", "Cube", attributes={"size": 1.0}) + sim_utils.delete_prim("/World/OldObject") + +**Legacy Support:** + +For backward compatibility, legacy functions are still available in ``isaaclab.sim.utils.legacy``, but it's recommended to migrate to the new APIs or use USD directly. + +**Full Changelog**: https://github.com/isaac-sim/IsaacLab/compare/v2.2.1...v2.3.2 + +v2.3.1 +====== + +What's Changed +-------------- + +This is a small patch release with a few critical fixes that impacted user workflows. + +Key fixes include: +* The behavior of termination logging has changed in the manager-based workflow, where ``get_done_term`` now returns the current step value instead of the last episode value. +* Additionally, a breaking change in the URDF importer was introduced in Isaac Sim 5.1, where the merge joints flag is no longer supported. We have now introduced a patch in the importer to return the behavior. Moving forward, we plan to deprecate this flag in favor of preserving asset definitions from URDFs directly without performing additional processing during the import process. + +Bug Fixes +--------- + +* Updates URDF importer to 2.4.31 to continue support for merge-joints by @kellyguo11 in https://github.com/isaac-sim/IsaacLab/pull/4000 +* Separates per-step termination and last-episode termination bookkeeping by @ooctipus in https://github.com/isaac-sim/IsaacLab/pull/3745 +* Uses effort_limit from USD if not specified in actuator cfg by @JuanaDd in https://github.com/isaac-sim/IsaacLab/pull/3522 +* Fixes type name for tendon properties in from_files config by @KyleM73 in https://github.com/isaac-sim/IsaacLab/pull/3941 +* Fixes duplicated text in pip installation docs by @shryt in https://github.com/isaac-sim/IsaacLab/pull/3969 +* Pins python version of pre-commmit.yaml workflow by @hhansen-bdai in https://github.com/isaac-sim/IsaacLab/pull/3929 + +Documentation +------------- + +* Updates the mimic teleop doc to link to the locomotion policy training by @huihuaNvidia2023 in https://github.com/isaac-sim/IsaacLab/pull/4053 + +**Full Changelog**: https://github.com/isaac-sim/IsaacLab/compare/v2.3.0...v2.3.1 + v2.3.0 ====== diff --git a/docs/source/refs/snippets/code_skeleton.py b/docs/source/refs/snippets/code_skeleton.py index cf0385279b6b..759ca38ae0f9 100644 --- a/docs/source/refs/snippets/code_skeleton.py +++ b/docs/source/refs/snippets/code_skeleton.py @@ -1,13 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 os -import sys from typing import ClassVar - DEFAULT_TIMEOUT: int = 30 """Default timeout for the task.""" diff --git a/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py b/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py index f0e9ba183f13..e839abf20099 100644 --- a/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py +++ b/docs/source/refs/snippets/tutorial_modify_direct_rl_env.py @@ -1,8 +1,9 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 # fmt: off # [start-init-import] diff --git a/docs/source/refs/troubleshooting.rst b/docs/source/refs/troubleshooting.rst index 18a88da7c698..8f3a82f3f150 100644 --- a/docs/source/refs/troubleshooting.rst +++ b/docs/source/refs/troubleshooting.rst @@ -75,8 +75,8 @@ For instance, to run a standalone script with verbose logging, you can use the f # Run the standalone script with info logging ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py --headless --info -For more fine-grained control, you can modify the logging channels through the ``omni.log`` module. -For more information, please refer to its `documentation `__. +For more fine-grained control, you can modify the logging channels through the ``logger`` module. +For more information, please refer to its `documentation `__. Observing long load times at the start of the simulation diff --git a/docs/source/setup/installation/cloud_installation.rst b/docs/source/setup/installation/cloud_installation.rst index 25572e74396e..c555377bb6c0 100644 --- a/docs/source/setup/installation/cloud_installation.rst +++ b/docs/source/setup/installation/cloud_installation.rst @@ -133,28 +133,28 @@ Next, run the deployment script for your preferred cloud: .. code-block:: bash - ./deploy-aws --isaaclab v2.3.0 + ./deploy-aws --isaaclab v2.3.2 .. tab-item:: Azure :sync: azure .. code-block:: bash - ./deploy-azure --isaaclab v2.3.0 + ./deploy-azure --isaaclab v2.3.2 .. tab-item:: GCP :sync: gcp .. code-block:: bash - ./deploy-gcp --isaaclab v2.3.0 + ./deploy-gcp --isaaclab v2.3.2 .. tab-item:: Alibaba Cloud :sync: alicloud .. code-block:: bash - ./deploy-alicloud --isaaclab v2.3.0 + ./deploy-alicloud --isaaclab v2.3.2 Follow the prompts for entering information regarding the environment setup and credentials. Once successful, instructions for connecting to the cloud instance will be available 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 3586ef61cef0..822df92e4d6e 100644 --- a/docs/source/setup/installation/include/pip_python_virtual_env.rst +++ b/docs/source/setup/installation/include/pip_python_virtual_env.rst @@ -32,10 +32,23 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. tab-set:: - .. tab-item:: UV Environment + .. tab-item:: Conda Environment - To install ``uv``, please follow the instructions `here `__. - You can create the Isaac Lab environment using the following commands: + To install conda, please follow the instructions `here `__. + You can create the Isaac Lab environment using the following commands. + + We recommend using `Miniconda `_, + since it is light-weight and resource-efficient environment management system. + + .. code-block:: bash + + conda create -n env_isaaclab python=3.11 + conda activate env_isaaclab + + .. tab-item:: venv Environment + + To create a virtual environment using the standard library, you can use the + following commands: .. tab-set:: :sync-group: os @@ -46,7 +59,7 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: bash # create a virtual environment named env_isaaclab with python3.11 - uv venv --python 3.11 env_isaaclab + python3.11 -m venv env_isaaclab # activate the virtual environment source env_isaaclab/bin/activate @@ -56,27 +69,21 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: batch :: create a virtual environment named env_isaaclab with python3.11 - uv venv --python 3.11 env_isaaclab + python3.11 -m venv env_isaaclab :: activate the virtual environment env_isaaclab\Scripts\activate - .. tab-item:: Conda Environment - - To install conda, please follow the instructions `here `__. - You can create the Isaac Lab environment using the following commands. - - We recommend using `Miniconda `_, - since it is light-weight and resource-efficient environment management system. + .. tab-item:: UV Environment (experimental) - .. code-block:: bash + To install ``uv``, please follow the instructions `here `__. - conda create -n env_isaaclab python=3.11 - conda activate env_isaaclab + .. note:: - .. tab-item:: venv Environment + A virtual environment created by ``uv venv`` does **not** include ``pip``. + Since Isaac Lab installation requires ``pip``, please install it manually + after activating the environment. - To create a virtual environment using the standard library, you can use the - following commands: + You can create the Isaac Lab environment using the following commands: .. tab-set:: :sync-group: os @@ -86,8 +93,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_isaaclab with python3.11 - python3.11 -m venv env_isaaclab + # create a virtual environment named env_isaaclab with python3.11 and pip + uv venv --python 3.11 --seed env_isaaclab # activate the virtual environment source env_isaaclab/bin/activate @@ -96,12 +103,13 @@ If you wish to install Isaac Sim 4.5, please use modify the instructions accordi .. code-block:: batch - :: create a virtual environment named env_isaaclab with python3.11 - python3.11 -m venv env_isaaclab + :: create a virtual environment named env_isaaclab with python3.11 and pip + uv venv --python 3.11 --seed env_isaaclab :: activate the virtual environment env_isaaclab\Scripts\activate + - Ensure the latest pip version is installed. To update pip, run the following command from inside the virtual environment: 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 d94d908d8319..6e995f2f0e89 100644 --- a/docs/source/setup/installation/include/src_python_virtual_env.rst +++ b/docs/source/setup/installation/include/src_python_virtual_env.rst @@ -35,10 +35,13 @@ instead of *./isaaclab.sh -p* or *isaaclab.bat -p*. .. tab-set:: - .. tab-item:: UV Environment + .. tab-item:: Conda Environment - To install ``uv``, please follow the instructions `here `__. - You can create the Isaac Lab environment using the following commands: + To install conda, please follow the instructions `here `__. + You can create the Isaac Lab environment using the following commands. + + We recommend using `Miniconda `_, + since it is light-weight and resource-efficient environment management system. .. tab-set:: :sync-group: os @@ -49,29 +52,34 @@ instead of *./isaaclab.sh -p* or *isaaclab.bat -p*. .. code:: bash # Option 1: Default environment name 'env_isaaclab' - ./isaaclab.sh --uv # or "./isaaclab.sh -u" + ./isaaclab.sh --conda # or "./isaaclab.sh -c" # Option 2: Custom name - ./isaaclab.sh --uv my_env # or "./isaaclab.sh -u my_env" + ./isaaclab.sh --conda my_env # or "./isaaclab.sh -c my_env" .. code:: bash # Activate environment - source ./env_isaaclab/bin/activate # or "source ./my_env/bin/activate" + conda activate env_isaaclab # or "conda activate my_env" .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows - .. warning:: - Windows support for UV is currently unavailable. Please check - `issue #3483 `_ to track progress. + .. code:: batch - .. tab-item:: Conda Environment + :: Option 1: Default environment name 'env_isaaclab' + isaaclab.bat --conda :: or "isaaclab.bat -c" + :: Option 2: Custom name + isaaclab.bat --conda my_env :: or "isaaclab.bat -c my_env" - To install conda, please follow the instructions `here `__. - You can create the Isaac Lab environment using the following commands. + .. code:: batch - We recommend using `Miniconda `_, - since it is light-weight and resource-efficient environment management system. + :: Activate environment + conda activate env_isaaclab # or "conda activate my_env" + + .. tab-item:: UV Environment (experimental) + + To install ``uv``, please follow the instructions `here `__. + You can create the Isaac Lab environment using the following commands: .. tab-set:: :sync-group: os @@ -82,29 +90,21 @@ instead of *./isaaclab.sh -p* or *isaaclab.bat -p*. .. code:: bash # Option 1: Default environment name 'env_isaaclab' - ./isaaclab.sh --conda # or "./isaaclab.sh -c" + ./isaaclab.sh --uv # or "./isaaclab.sh -u" # Option 2: Custom name - ./isaaclab.sh --conda my_env # or "./isaaclab.sh -c my_env" + ./isaaclab.sh --uv my_env # or "./isaaclab.sh -u my_env" .. code:: bash # Activate environment - conda activate env_isaaclab # or "conda activate my_env" + source ./env_isaaclab/bin/activate # or "source ./my_env/bin/activate" .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows - .. code:: batch - - :: Option 1: Default environment name 'env_isaaclab' - isaaclab.bat --conda :: or "isaaclab.bat -c" - :: Option 2: Custom name - isaaclab.bat --conda my_env :: or "isaaclab.bat -c my_env" - - .. code:: batch - - :: Activate environment - conda activate env_isaaclab # or "conda activate my_env" + .. warning:: + Windows support for UV is currently unavailable. Please check + `issue #3483 `_ to track progress. Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` or ``isaaclab.bat -p`` to run python scripts. You can use the default python executable in your diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst index f2aed3ef048a..725f31f9bdcc 100644 --- a/docs/source/setup/installation/index.rst +++ b/docs/source/setup/installation/index.rst @@ -80,19 +80,15 @@ Other notable limitations with respect to Isaac Lab include... #. `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 +#. Extended reality teleoperation tools such as `OpenXR `_ is not supported. This is due to encoding performance limitations that have not yet been fully investigated. -#. SKRL training with JAX _ has not been explicitly validated or tested in Isaac Lab on the DGX Spark. +#. SKRL training with `JAX `_ has not been explicitly validated or tested in Isaac 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. GPU support requires building JAX from source, which has not been validated in Isaac Lab. #. Livestream and Hub Workstation Cache are not supported on the DGX spark. -#. Multi-node training may require direct connections between Spark machines or additional network configurations. - -#. :ref:`Isaac Lab Mimic ` data generation and policy inference for visuomotor environments are not supported on DGX Spark due to a lack of non-DLSS image denoiser on aarch64. - #. :ref:`Running Cosmos Transfer1 ` is not currently supported on the DGX Spark. Troubleshooting diff --git a/docs/source/setup/installation/isaaclab_pip_installation.rst b/docs/source/setup/installation/isaaclab_pip_installation.rst index 29e4b2c6dc07..bddc2a3bfaa4 100644 --- a/docs/source/setup/installation/isaaclab_pip_installation.rst +++ b/docs/source/setup/installation/isaaclab_pip_installation.rst @@ -25,35 +25,13 @@ Installing dependencies In case you used UV to create your virtual environment, please replace ``pip`` with ``uv pip`` in the following commands. -- Install a CUDA-enabled PyTorch 2.7.0 build for CUDA 12.8: - - .. code-block:: none - - pip install torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 - -- If you want to use ``rl_games`` for training and inferencing, install the - its Python 3.11 enabled fork: - - .. code-block:: none - - pip install git+https://github.com/isaac-sim/rl_games.git@python3.11 - -- Install the Isaac Lab packages along with Isaac Sim: - - .. code-block:: none - - pip install isaaclab[isaacsim,all]==2.2.0 --extra-index-url https://pypi.nvidia.com - - In case you used UV to create your virtual environment, please replace ``pip`` with ``uv pip`` - in the following commands. - - Install the Isaac Lab packages along with Isaac Sim: .. code-block:: none - pip install isaaclab[isaacsim,all]==2.3.0 --extra-index-url https://pypi.nvidia.com + pip install isaaclab[isaacsim,all]==2.3.2 --extra-index-url https://pypi.nvidia.com -- Install a CUDA-enabled PyTorch build that matches your system architecture: +- Install a CUDA-enabled PyTorch 2.7.0 build for CUDA 12.8 that matches your system architecture: .. tab-set:: :sync-group: pip-platform diff --git a/docs/source/setup/quickstart.rst b/docs/source/setup/quickstart.rst index 09f7bc8e26f8..70b5c7a7c6ea 100644 --- a/docs/source/setup/quickstart.rst +++ b/docs/source/setup/quickstart.rst @@ -35,12 +35,12 @@ To begin, we first define our virtual environment. .. code-block:: bash - # create a virtual environment named env_isaaclab with python3.11 + # create a virtual environment named env_isaaclab with python3.11 and pip conda create -n env_isaaclab python=3.11 # activate the virtual environment conda activate env_isaaclab - .. tab-item:: uv + .. tab-item:: uv (experimental) .. tab-set:: :sync-group: os @@ -50,8 +50,8 @@ To begin, we first define our virtual environment. .. code-block:: bash - # create a virtual environment named env_isaaclab with python3.11 - uv venv --python 3.11 env_isaaclab + # create a virtual environment named env_isaaclab with python3.11 and pip + uv venv --python 3.11 --seed env_isaaclab # activate the virtual environment source env_isaaclab/bin/activate @@ -134,6 +134,22 @@ Installation is now as easy as navigating to the repo and then calling the root isaaclab.bat --install :: or "isaaclab.bat -i" +Quick Start Using Isaac Launchable +---------------------------------- + +For users first learning Isaac Lab, without sufficient local compute resources, the `Isaac Launchable `_ project is a quick way to get started without manual installation. + +Through this project, users can interact with Isaac Sim and Isaac Lab purely from a web browser, with one tab running Visual Studio Code for development and command execution, and another tab providing the streamed user interface for Isaac Sim. + +This method uses `NVIDIA Brev `_, a platform that offers easily configurable pay-by-the-hour cloud compute. Brev Launchables are preconfigured, optimized compute and software environments. + +To try now, click the button below. To learn more about how to use this project, or how to create your own Launchable, please see the project repo `here `_. + +.. image:: https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-dark.svg + :target: https://brev.nvidia.com/launchable/deploy/now?launchableID=env-35JP2ywERLgqtD0b0MIeK1HnF46 + :alt: Click here to deploy + + Launch Training ------------------- diff --git a/docs/source/tutorials/00_sim/spawn_prims.rst b/docs/source/tutorials/00_sim/spawn_prims.rst index caeafa714b31..66941ddb1c7f 100644 --- a/docs/source/tutorials/00_sim/spawn_prims.rst +++ b/docs/source/tutorials/00_sim/spawn_prims.rst @@ -114,7 +114,7 @@ Here we make an Xform prim to group all the primitive shapes under it. .. literalinclude:: ../../../../scripts/tutorials/00_sim/spawn_prims.py :language: python :start-at: # create a new xform prim for all objects to be spawned under - :end-at: prim_utils.create_prim("/World/Objects", "Xform") + :end-at: sim_utils.create_prim("/World/Objects", "Xform") Next, we spawn a cone using the :class:`~sim.spawners.shapes.ConeCfg` class. It is possible to specify the radius, height, physics properties, and material properties of the cone. By default, the physics and material diff --git a/docs/source/tutorials/04_sensors/add_sensors_on_robot.rst b/docs/source/tutorials/04_sensors/add_sensors_on_robot.rst index e5815e800a55..3d9f40667b62 100644 --- a/docs/source/tutorials/04_sensors/add_sensors_on_robot.rst +++ b/docs/source/tutorials/04_sensors/add_sensors_on_robot.rst @@ -53,7 +53,7 @@ in seconds through the :attr:`sensors.SensorBaseCfg.update_period` attribute. Depending on the specified path and the sensor type, the sensors are attached to the prims in the scene. They may have an associated prim that is created in the scene or they may be attached to an existing prim. For instance, the camera sensor has a corresponding prim that is created in the scene, whereas for the -contact sensor, the activating the contact reporting is a property on a rigid body prim. +contact sensor, activating the contact reporting is a property on a rigid body prim. In the following, we introduce the different sensors we use in this tutorial and how they are configured. For more description about them, please check the :mod:`sensors` module. diff --git a/environment.yml b/environment.yml index d51e2fc4db7a..053fef4e99db 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/greptile.json b/greptile.json new file mode 100644 index 000000000000..1f195339e5b7 --- /dev/null +++ b/greptile.json @@ -0,0 +1,3 @@ +{ + "triggerOnUpdates": false +} diff --git a/isaaclab.sh b/isaaclab.sh index c5bde803f4d6..00008d6ec827 100755 --- a/isaaclab.sh +++ b/isaaclab.sh @@ -89,11 +89,9 @@ PY # check if running in docker is_docker() { - [ -f /.dockerenv ] || \ - grep -q docker /proc/1/cgroup || \ - [[ $(cat /proc/1/comm) == "containerd-shim" ]] || \ - grep -q docker /proc/mounts || \ - [[ "$(hostname)" == *"."* ]] + [ -f /.dockerenv ] || [ -f /run/.containerenv ] || \ + grep -qaE '(docker|containerd|kubepods|podman)' /proc/1/cgroup || \ + [[ $(cat /proc/1/comm) == "containerd-shim" ]] } # check if running on ARM architecture @@ -102,9 +100,10 @@ is_arm() { } ensure_cuda_torch() { - local py="$1" - - # base base index for torch + local python_exe=$(extract_python_exe) + local pip_install_command=$(extract_pip_command) + local pip_uninstall_command=$(extract_pip_uninstall_command) + # base index for torch local base_index="https://download.pytorch.org/whl" # choose pins per arch @@ -124,9 +123,15 @@ ensure_cuda_torch() { # check current torch version (may be empty) local cur="" - if "$py" -m pip show torch >/dev/null 2>&1; then - cur="$("$py" -m pip show torch 2>/dev/null | awk -F': ' '/^Version/{print $2}')" - fi + cur="$(${python_exe} - <<'PY' 2>/dev/null || true +try: + import torch +except Exception: + pass +else: + print(torch.__version__, end="") +PY +)" # skip install if version is already satisfied if [[ "$cur" == "$want_torch" ]]; then @@ -135,8 +140,8 @@ ensure_cuda_torch() { # clean install torch echo "[INFO] Installing torch==${torch_ver} and torchvision==${tv_ver} (cu${cuda_ver}) from ${index}..." - "$py" -m pip uninstall -y torch torchvision torchaudio >/dev/null 2>&1 || true - "$py" -m pip install -U --index-url "${index}" "torch==${torch_ver}" "torchvision==${tv_ver}" + ${pip_uninstall_command} torch torchvision torchaudio >/dev/null 2>&1 || true + ${pip_install_command} -U --index-url "${index}" "torch==${torch_ver}" "torchvision==${tv_ver}" } # extract isaac sim path @@ -568,7 +573,7 @@ while [[ $# -gt 0 ]]; do begin_arm_install_sandbox # install pytorch (version based on arch) - ensure_cuda_torch ${python_exe} + ensure_cuda_torch # recursively look into directories and install them # this does not check dependencies between extensions export -f extract_python_exe @@ -598,7 +603,7 @@ while [[ $# -gt 0 ]]; do # in some rare cases, torch might not be installed properly by setup.py, add one more check here # can prevent that from happening - ensure_cuda_torch ${python_exe} + ensure_cuda_torch # restore LD_PRELOAD if we cleared it end_arm_install_sandbox diff --git a/pyproject.toml b/pyproject.toml index aa5574018eb0..f52c70333d47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,67 +1,104 @@ -[tool.isort] - -py_version = 310 -line_length = 120 -group_by_package = true - -# Files to skip -skip_glob = ["docs/*", "logs/*", "_isaac_sim/*", ".vscode/*"] - -# Order of imports -sections = [ - "FUTURE", - "STDLIB", - "THIRDPARTY", - "ASSETS_FIRSTPARTY", - "FIRSTPARTY", - "EXTRA_FIRSTPARTY", - "TASK_FIRSTPARTY", - "LOCALFOLDER", +# 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 + +[tool.ruff] +line-length = 120 +target-version = "py310" + +# Exclude directories +extend-exclude = [ + "logs", + "_isaac_sim", + ".vscode", + "_*", + ".git", +] + +[tool.ruff.lint] +# Enable flake8 rules and other useful ones +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "C90", # mccabe complexity + # "D", # pydocstyle + "SIM", # flake8-simplify + "RET", # flake8-return ] -# Extra standard libraries considered as part of python (permissive licenses -extra_standard_library = [ - "numpy", - "h5py", - "open3d", - "torch", - "tensordict", - "bpy", - "matplotlib", - "gymnasium", - "gym", - "scipy", - "hid", - "yaml", - "prettytable", - "toml", - "trimesh", - "tqdm", - "torchvision", - "transformers", - "einops" # Needed for transformers, doesn't always auto-install +# Ignore specific rules (matching your flake8 config) +ignore = [ + "E402", # Module level import not at top of file + "D401", # First line should be in imperative mood + "RET504", # Unnecessary variable assignment before return statement + "RET505", # Unnecessary elif after return statement + "SIM102", # Use a single if-statement instead of nested if-statements + "SIM103", # Return the negated condition directly + "SIM108", # Use ternary operator instead of if-else statement + "SIM117", # Merge with statements for context managers + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "UP006", # Use 'dict' instead of 'Dict' type annotation + "UP018", # Unnecessary `float` call (rewrite as a literal) +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # Allow unused imports in __init__.py files + +[tool.ruff.lint.mccabe] +max-complexity = 30 + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.isort] + +# Custom import sections with separate sections for each Isaac Lab extension +section-order = [ + "future", + "standard-library", + "third-party", + # Group omniverse extensions separately since they are run-time dependencies + # which are pulled in by Isaac Lab extensions + "omniverse-extensions", + # Group Isaac Lab extensions together since they are all part of the Isaac Lab project + "isaaclab", + "isaaclab-contrib", + "isaaclab-rl", + "isaaclab-mimic", + "isaaclab-tasks", + "isaaclab-assets", + # First-party is reserved for project templates + "first-party", + "local-folder", ] -# Imports from Isaac Sim and Omniverse -known_third_party = [ - "isaacsim.core.api", - "isaacsim.replicator.common", - "omni.replicator.core", + +[tool.ruff.lint.isort.sections] +# Define what belongs in each custom section + +"omniverse-extensions" = [ + "isaacsim", + "omni", "pxr", - "omni.kit.*", - "warp", "carb", + "usdrt", "Semantics", + "curobo", ] -# Imports from this repository -known_first_party = "isaaclab" -known_assets_firstparty = "isaaclab_assets" -known_extra_firstparty = [ - "isaaclab_rl", - "isaaclab_mimic", -] -known_task_firstparty = "isaaclab_tasks" -# Imports from the local folder -known_local_folder = "config" + +"isaaclab" = ["isaaclab"] +"isaaclab-assets" = ["isaaclab_assets"] +"isaaclab-contrib" = ["isaaclab_contrib"] +"isaaclab-rl" = ["isaaclab_rl"] +"isaaclab-mimic" = ["isaaclab_mimic"] +"isaaclab-tasks" = ["isaaclab_tasks"] + +[tool.ruff.format] + +docstring-code-format = true [tool.pyright] @@ -92,9 +129,14 @@ reportPrivateUsage = "warning" [tool.codespell] -skip = '*.usd,*.svg,*.png,_isaac_sim*,*.bib,*.css,*/_build' +skip = '*.usd,*.usda,*.usdz,*.svg,*.png,_isaac_sim*,*.bib,*.css,*/_build' quiet-level = 0 # the world list should always have words in lower case -ignore-words-list = "haa,slq,collapsable,buss,reacher" -# todo: this is hack to deal with incorrect spelling of "Environment" in the Isaac Sim grid world asset -exclude-file = "source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py" +ignore-words-list = "haa,slq,collapsable,buss,reacher,thirdparty" + + +[tool.pytest.ini_options] + +markers = [ + "isaacsim_ci: mark test to run in isaacsim ci", +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index dd4d14daf944..000000000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -markers = - isaacsim_ci: mark test to run in isaacsim ci diff --git a/scripts/benchmarks/benchmark_cameras.py b/scripts/benchmarks/benchmark_cameras.py index f8ec527ef291..a5d6a0c00267 100644 --- a/scripts/benchmarks/benchmark_cameras.py +++ b/scripts/benchmarks/benchmark_cameras.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -239,15 +239,13 @@ """Rest everything follows.""" -import gymnasium as gym -import numpy as np import random import time -import torch -import isaacsim.core.utils.prims as prim_utils +import gymnasium as gym +import numpy as np import psutil -from isaacsim.core.utils.stage import create_new_stage +import torch import isaaclab.sim as sim_utils from isaaclab.assets import RigidObject, RigidObjectCfg @@ -286,7 +284,7 @@ def create_camera_base( if instantiate: # Create the necessary prims for idx in range(num_cams): - prim_utils.create_prim(f"/World/{name}_{idx:02d}", "Xform") + sim_utils.create_prim(f"/World/{name}_{idx:02d}", "Xform") if prim_path is None: prim_path = f"/World/{name}_.*/{name}" # If valid camera settings are provided, create the camera @@ -346,7 +344,7 @@ def create_ray_caster_cameras( ) -> RayCasterCamera | RayCasterCameraCfg | None: """Create the raycaster cameras; different configuration than Standard/Tiled camera""" for idx in range(num_cams): - prim_utils.create_prim(f"/World/RayCasterCamera_{idx:02d}/RayCaster", "Xform") + sim_utils.create_prim(f"/World/RayCasterCamera_{idx:02d}/RayCaster", "Xform") if num_cams > 0 and len(data_types) > 0 and height > 0 and width > 0: cam_cfg = RayCasterCameraCfg( @@ -446,7 +444,7 @@ def design_scene( 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(num_objects): # sample random position @@ -548,7 +546,6 @@ def get_utilization_percentages(reset: bool = False, max_values: list[float] = [ # GPU utilization using pynvml if torch.cuda.is_available(): - if args_cli.autotune: pynvml.nvmlInit() # Initialize NVML for i in range(torch.cuda.device_count()): @@ -665,7 +662,6 @@ def run_simulator( # Loop through all camera lists and their data_types for camera_list, data_types, label in zip(camera_lists, camera_data_types, labels): for cam_idx, camera in enumerate(camera_list): - if env is None: # No env, need to step cams manually # Only update the camera if it hasn't been updated as part of scene_entities.update ... camera.update(dt=sim.get_physics_dt()) @@ -851,7 +847,7 @@ def main(): cur_sys_util = analysis["system_utilization_analytics"] print("Triggering reset...") env.close() - create_new_stage() + sim_utils.create_new_stage() print("[INFO]: DONE! Feel free to CTRL + C Me ") print(f"[INFO]: If you've made it this far, you can likely simulate {cur_num_cams} {camera_name_prefix}") print("Keep in mind, this is without any training running on the GPU.") diff --git a/scripts/benchmarks/benchmark_load_robot.py b/scripts/benchmarks/benchmark_load_robot.py index 9ddddf652340..45d066162c8f 100644 --- a/scripts/benchmarks/benchmark_load_robot.py +++ b/scripts/benchmarks/benchmark_load_robot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/benchmarks/benchmark_non_rl.py b/scripts/benchmarks/benchmark_non_rl.py index adc797e7f5e3..20d4221bc30e 100644 --- a/scripts/benchmarks/benchmark_non_rl.py +++ b/scripts/benchmarks/benchmark_non_rl.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -64,6 +64,7 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../..")) from isaaclab.utils.timer import Timer + from scripts.benchmarks.utils import ( log_app_start_time, log_python_imports_time, @@ -76,11 +77,12 @@ imports_time_begin = time.perf_counter_ns() +import os +from datetime import datetime + import gymnasium as gym import numpy as np -import os import torch -from datetime import datetime from isaaclab.envs import DirectMARLEnvCfg, DirectRLEnvCfg, ManagerBasedRLEnvCfg from isaaclab.utils.dict import print_dict diff --git a/scripts/benchmarks/benchmark_rlgames.py b/scripts/benchmarks/benchmark_rlgames.py index c142af861859..b3f20ecd02a4 100644 --- a/scripts/benchmarks/benchmark_rlgames.py +++ b/scripts/benchmarks/benchmark_rlgames.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -62,13 +62,13 @@ imports_time_begin = time.perf_counter_ns() -import gymnasium as gym import math import os import random -import torch from datetime import datetime +import gymnasium as gym +import torch from rl_games.common import env_configurations, vecenv from rl_games.common.algo_observer import IsaacAlgoObserver from rl_games.torch_runner import Runner @@ -87,6 +87,7 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../..")) from isaaclab.utils.timer import Timer + from scripts.benchmarks.utils import ( log_app_start_time, log_python_imports_time, diff --git a/scripts/benchmarks/benchmark_rsl_rl.py b/scripts/benchmarks/benchmark_rsl_rl.py index 506559fb442f..8e3b4e132a59 100644 --- a/scripts/benchmarks/benchmark_rsl_rl.py +++ b/scripts/benchmarks/benchmark_rsl_rl.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -66,11 +66,11 @@ imports_time_begin = time.perf_counter_ns() +from datetime import datetime + import gymnasium as gym import numpy as np import torch -from datetime import datetime - from rsl_rl.runners import OnPolicyRunner from isaaclab.envs import DirectMARLEnvCfg, DirectRLEnvCfg, ManagerBasedRLEnvCfg @@ -91,6 +91,7 @@ from isaacsim.benchmark.services import BaseIsaacBenchmark from isaaclab.utils.timer import Timer + from scripts.benchmarks.utils import ( log_app_start_time, log_python_imports_time, diff --git a/scripts/benchmarks/benchmark_view_comparison.py b/scripts/benchmarks/benchmark_view_comparison.py new file mode 100644 index 000000000000..8fca68312ab5 --- /dev/null +++ b/scripts/benchmarks/benchmark_view_comparison.py @@ -0,0 +1,512 @@ +# 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 + +"""Benchmark script comparing XformPrimView vs PhysX RigidBodyView for transform operations. + +This script tests the performance of batched transform operations using: + +- Isaac Lab's XformPrimView (USD-based) +- Isaac Lab's XformPrimView (Fabric-based) +- PhysX RigidBodyView (PhysX tensors-based, as used in RigidObject) + +Note: + XformPrimView operates on USD attributes directly (useful for non-physics prims), + or on Fabric attributes when Fabric is enabled. + while RigidBodyView requires rigid body physics components and operates on PhysX tensors. + This benchmark helps understand the performance trade-offs between the two approaches. + +Usage: + # Basic benchmark + ./isaaclab.sh -p scripts/benchmarks/benchmark_view_comparison.py --num_envs 1024 --device cuda:0 --headless + + # With profiling enabled (for snakeviz visualization) + ./isaaclab.sh -p scripts/benchmarks/benchmark_view_comparison.py --num_envs 1024 --profile --headless + + # Then visualize with snakeviz: + snakeviz profile_results/xform_view_benchmark.prof + snakeviz profile_results/physx_view_benchmark.prof +""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# parse the arguments +args_cli = argparse.Namespace() + +parser = argparse.ArgumentParser(description="Benchmark XformPrimView vs PhysX RigidBodyView performance.") + +parser.add_argument("--num_envs", type=int, default=1000, help="Number of environments to simulate.") +parser.add_argument("--num_iterations", type=int, default=50, help="Number of iterations for each test.") +parser.add_argument( + "--profile", + action="store_true", + help="Enable profiling with cProfile. Results saved as .prof files for snakeviz visualization.", +) +parser.add_argument( + "--profile-dir", + type=str, + default="./profile_results", + help="Directory to save profile results. Default: ./profile_results", +) + +AppLauncher.add_app_launcher_args(parser) +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import cProfile +import time + +import torch + +from isaacsim.core.simulation_manager import SimulationManager + +import isaaclab.sim as sim_utils +import isaaclab.utils.math as math_utils +from isaaclab.sim.views import XformPrimView + + +@torch.no_grad() +def benchmark_view(view_type: str, num_iterations: int) -> tuple[dict[str, float], dict[str, torch.Tensor]]: + """Benchmark the specified view class. + + Args: + view_type: Type of view to benchmark ("xform", "xform_fabric", or "physx"). + num_iterations: Number of iterations to run. + + Returns: + A tuple of (timing_results, computed_results) where: + - timing_results: Dictionary containing timing results for various operations + - computed_results: Dictionary containing the computed values for validation + """ + timing_results = {} + computed_results = {} + + # Setup scene + print(" Setting up scene") + # Clear stage + sim_utils.create_new_stage() + # Create simulation context + start_time = time.perf_counter() + sim_cfg = sim_utils.SimulationCfg(dt=0.01, device=args_cli.device, use_fabric=(view_type == "xform_fabric")) + sim = sim_utils.SimulationContext(sim_cfg) + stage = sim_utils.get_current_stage() + + print(f" Time taken to create simulation context: {time.perf_counter() - start_time:.4f} seconds") + + # create a rigid object + object_cfg = sim_utils.ConeCfg( + radius=0.15, + height=0.5, + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), + ) + # Create prims + for i in range(args_cli.num_envs): + sim_utils.create_prim(f"/World/Env_{i}", "Xform", stage=stage, translation=(i * 2.0, 0.0, 0.0)) + object_cfg.func(f"/World/Env_{i}/Object", object_cfg, translation=(0.0, 0.0, 1.0)) + + # Play simulation + sim.reset() + + # Pattern to match all prims + pattern = "/World/Env_.*/Object" if view_type == "xform" else "/World/Env_*/Object" + print(f" Pattern: {pattern}") + + # Create view based on type + start_time = time.perf_counter() + if view_type == "xform": + view = XformPrimView(pattern, device=args_cli.device, validate_xform_ops=False) + num_prims = view.count + view_name = "XformPrimView (USD)" + elif view_type == "xform_fabric": + if "cuda" not in args_cli.device: + raise ValueError("Fabric backend requires CUDA. Please use --device cuda:0 for this benchmark.") + view = XformPrimView(pattern, device=args_cli.device, validate_xform_ops=False) + num_prims = view.count + view_name = "XformPrimView (Fabric)" + else: # physx + physics_sim_view = SimulationManager.get_physics_sim_view() + view = physics_sim_view.create_rigid_body_view(pattern) + num_prims = view.count + view_name = "PhysX RigidBodyView" + timing_results["init"] = time.perf_counter() - start_time + # prepare indices for benchmarking + all_indices = torch.arange(num_prims, device=args_cli.device) + + print(f" {view_name} managing {num_prims} prims") + + # Fabric is write-first: initialize it to match USD before benchmarking reads. + if view_type == "xform_fabric" and num_prims > 0: + init_positions = torch.zeros((num_prims, 3), dtype=torch.float32, device=args_cli.device) + init_positions[:, 0] = 2.0 * torch.arange(num_prims, device=args_cli.device, dtype=torch.float32) + init_positions[:, 2] = 1.0 + init_orientations = torch.tensor( + [[1.0, 0.0, 0.0, 0.0]] * num_prims, dtype=torch.float32, device=args_cli.device + ) + view.set_world_poses(init_positions, init_orientations) + + # Benchmark get_world_poses + start_time = time.perf_counter() + for _ in range(num_iterations): + if view_type in ("xform", "xform_fabric"): + positions, orientations = view.get_world_poses() + else: # physx + transforms = view.get_transforms() + positions = transforms[:, :3] + orientations = transforms[:, 3:7] + # Convert quaternion from xyzw to wxyz + orientations = math_utils.convert_quat(orientations, to="wxyz") + timing_results["get_world_poses"] = (time.perf_counter() - start_time) / num_iterations + + # Store initial world poses + computed_results["initial_world_positions"] = positions.clone() + computed_results["initial_world_orientations"] = orientations.clone() + + # Benchmark set_world_poses + new_positions = positions.clone() + new_positions[:, 2] += 0.5 + start_time = time.perf_counter() + for _ in range(num_iterations): + if view_type in ("xform", "xform_fabric"): + view.set_world_poses(new_positions, orientations) + else: # physx + # Convert quaternion from wxyz to xyzw for PhysX + orientations_xyzw = math_utils.convert_quat(orientations, to="xyzw") + new_transforms = torch.cat([new_positions, orientations_xyzw], dim=-1) + view.set_transforms(new_transforms, indices=all_indices) + timing_results["set_world_poses"] = (time.perf_counter() - start_time) / num_iterations + + # Get world poses after setting to verify + if view_type in ("xform", "xform_fabric"): + positions_after_set, orientations_after_set = view.get_world_poses() + else: # physx + transforms_after = view.get_transforms() + positions_after_set = transforms_after[:, :3] + orientations_after_set = math_utils.convert_quat(transforms_after[:, 3:7], to="wxyz") + computed_results["world_positions_after_set"] = positions_after_set.clone() + computed_results["world_orientations_after_set"] = orientations_after_set.clone() + + # close simulation + sim.clear() + sim.clear_all_callbacks() + sim.clear_instance() + + return timing_results, computed_results + + +def compare_results( + results_dict: dict[str, dict[str, torch.Tensor]], tolerance: float = 1e-4 +) -> dict[str, dict[str, dict[str, float]]]: + """Compare computed results across implementations. + + Args: + results_dict: Dictionary mapping implementation names to their computed values. + tolerance: Tolerance for numerical comparison. + + Returns: + Nested dictionary: {comparison_pair: {metric: {stats}}} + """ + comparison_stats = {} + impl_names = list(results_dict.keys()) + + # Compare each pair of implementations + for i, impl1 in enumerate(impl_names): + for impl2 in impl_names[i + 1 :]: + pair_key = f"{impl1}_vs_{impl2}" + comparison_stats[pair_key] = {} + + computed1 = results_dict[impl1] + computed2 = results_dict[impl2] + + for key in computed1.keys(): + if key not in computed2: + continue + + val1 = computed1[key] + val2 = computed2[key] + + # Skip zero tensors (not applicable tests) + if torch.all(val1 == 0) or torch.all(val2 == 0): + continue + + # Compute differences + diff = torch.abs(val1 - val2) + max_diff = torch.max(diff).item() + mean_diff = torch.mean(diff).item() + + # Check if within tolerance + all_close = torch.allclose(val1, val2, atol=tolerance, rtol=0) + + comparison_stats[pair_key][key] = { + "max_diff": max_diff, + "mean_diff": mean_diff, + "all_close": all_close, + } + + return comparison_stats + + +def print_comparison_results(comparison_stats: dict[str, dict[str, dict[str, float]]], tolerance: float): + """Print comparison results. + + Args: + comparison_stats: Nested dictionary containing comparison statistics. + tolerance: Tolerance used for comparison. + """ + for pair_key, pair_stats in comparison_stats.items(): + if not pair_stats: # Skip if no comparable results + continue + + # Format the pair key for display + impl1, impl2 = pair_key.split("_vs_") + display_impl1 = impl1.replace("_", " ").title() + display_impl2 = impl2.replace("_", " ").title() + comparison_title = f"{display_impl1} vs {display_impl2}" + + # Check if all results match + all_match = all(stats["all_close"] for stats in pair_stats.values()) + + if all_match: + # Compact output when everything matches + print("\n" + "=" * 100) + print(f"RESULT COMPARISON: {comparison_title}") + print("=" * 100) + print(f"✓ All computed values match within tolerance ({tolerance})") + print("=" * 100) + else: + # Detailed output when there are mismatches + print("\n" + "=" * 100) + print(f"RESULT COMPARISON: {comparison_title}") + print("=" * 100) + print(f"{'Computed Value':<40} {'Max Diff':<15} {'Mean Diff':<15} {'Match':<10}") + print("-" * 100) + + for key, stats in pair_stats.items(): + # Format the key for display + display_key = key.replace("_", " ").title() + match_str = "✓ Yes" if stats["all_close"] else "✗ No" + + print(f"{display_key:<40} {stats['max_diff']:<15.6e} {stats['mean_diff']:<15.6e} {match_str:<10}") + + print("=" * 100) + print(f"\n✗ Some results differ beyond tolerance ({tolerance})") + print(f" This may indicate implementation differences between {display_impl1} and {display_impl2}") + + print() + + +def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num_iterations: int): + """Print benchmark results in a formatted table. + + Args: + results_dict: Dictionary mapping implementation names to their timing results. + num_prims: Number of prims tested. + num_iterations: Number of iterations run. + """ + print("\n" + "=" * 100) + print(f"BENCHMARK RESULTS: {num_prims} prims, {num_iterations} iterations") + print("=" * 100) + + impl_names = list(results_dict.keys()) + # Format names for display + display_names = [name.replace("_", " ").title() for name in impl_names] + + # Calculate column width + col_width = 20 + + # Print header + header = f"{'Operation':<30}" + for display_name in display_names: + header += f" {display_name + ' (ms)':<{col_width}}" + print(header) + print("-" * 100) + + # Print each operation + operations = [ + ("Initialization", "init"), + ("Get World Poses", "get_world_poses"), + ("Set World Poses", "set_world_poses"), + ] + + for op_name, op_key in operations: + row = f"{op_name:<30}" + for impl_name in impl_names: + impl_time = results_dict[impl_name].get(op_key, 0) * 1000 # Convert to ms + row += f" {impl_time:>{col_width - 1}.4f}" + print(row) + + print("=" * 100) + + # Calculate and print total time (excluding N/A operations) + total_row = f"{'Total Time':<30}" + for impl_name in impl_names: + if impl_name == "physx_view": + # Exclude local pose operations for PhysX + total_time = ( + results_dict[impl_name].get("init", 0) * 1000 + + results_dict[impl_name].get("get_world_poses", 0) * 1000 + + results_dict[impl_name].get("set_world_poses", 0) * 1000 + ) + else: + total_time = sum(results_dict[impl_name].values()) * 1000 + total_row += f" {total_time:>{col_width - 1}.4f}" + print(f"\n{total_row}") + + # Calculate speedups relative to XformPrimView (USD baseline) + if "xform_view" in impl_names: + print("\n" + "=" * 100) + print("SPEEDUP vs XformPrimView (USD)") + print("=" * 100) + print(f"{'Operation':<30}", end="") + for impl_name, display_name in zip(impl_names, display_names): + if impl_name != "xform_view": + print(f" {display_name + ' Speedup':<{col_width}}", end="") + print() + print("-" * 100) + + xform_results = results_dict["xform_view"] + for op_name, op_key in operations: + print(f"{op_name:<30}", end="") + xform_time = xform_results.get(op_key, 0) + for impl_name, display_name in zip(impl_names, display_names): + if impl_name != "xform_view": + impl_time = results_dict[impl_name].get(op_key, 0) + if xform_time > 0 and impl_time > 0: + speedup = impl_time / xform_time + print(f" {speedup:>{col_width - 1}.2f}x", end="") + else: + print(f" {'N/A':>{col_width}}", end="") + print() + + # Overall speedup (only world pose operations) + print("=" * 100) + print(f"{'Overall Speedup (World Ops)':<30}", end="") + total_xform = ( + xform_results.get("init", 0) + + xform_results.get("get_world_poses", 0) + + xform_results.get("set_world_poses", 0) + ) + for impl_name, display_name in zip(impl_names, display_names): + if impl_name != "xform_view": + total_impl = ( + results_dict[impl_name].get("init", 0) + + results_dict[impl_name].get("get_world_poses", 0) + + results_dict[impl_name].get("set_world_poses", 0) + ) + if total_xform > 0 and total_impl > 0: + overall_speedup = total_impl / total_xform + print(f" {overall_speedup:>{col_width - 1}.2f}x", end="") + else: + print(f" {'N/A':>{col_width}}", end="") + print() + + print("\n" + "=" * 100) + print("\nNotes:") + print(" - Times are averaged over all iterations") + print(" - Speedup = (Implementation time) / (XformPrimView USD time)") + print(" - Speedup > 1.0 means USD XformPrimView is faster") + print(" - Speedup < 1.0 means the implementation is faster than USD") + print(" - PhysX View requires rigid body physics components") + print(" - XformPrimView works with any Xform prim (physics or non-physics)") + print(" - PhysX View does not support local pose operations directly") + print() + + +def main(): + """Main benchmark function.""" + print("=" * 100) + print("View Comparison Benchmark - XformPrimView vs PhysX RigidBodyView") + print("=" * 100) + print("Configuration:") + print(f" Number of environments: {args_cli.num_envs}") + print(f" Iterations per test: {args_cli.num_iterations}") + print(f" Device: {args_cli.device}") + print(f" Profiling: {'Enabled' if args_cli.profile else 'Disabled'}") + if args_cli.profile: + print(f" Profile directory: {args_cli.profile_dir}") + print() + + # Create profile directory if profiling is enabled + if args_cli.profile: + import os + + os.makedirs(args_cli.profile_dir, exist_ok=True) + + # Dictionary to store all results + all_timing_results = {} + all_computed_results = {} + profile_files = {} + + # Implementations to benchmark + implementations = [ + ("xform_view", "XformPrimView (USD)", "xform"), + ("xform_fabric_view", "XformPrimView (Fabric)", "xform_fabric"), + ("physx_view", "PhysX RigidBodyView", "physx"), + ] + + # Benchmark each implementation + for impl_key, impl_name, view_type in implementations: + print(f"Benchmarking {impl_name}...") + + if args_cli.profile: + profiler = cProfile.Profile() + profiler.enable() + + timing, computed = benchmark_view(view_type=view_type, num_iterations=args_cli.num_iterations) + + if args_cli.profile: + profiler.disable() + profile_file = f"{args_cli.profile_dir}/{impl_key}_benchmark.prof" + profiler.dump_stats(profile_file) + profile_files[impl_key] = profile_file + print(f" Profile saved to: {profile_file}") + + all_timing_results[impl_key] = timing + all_computed_results[impl_key] = computed + + print(" Done!") + print() + + # Print timing results + print_results(all_timing_results, args_cli.num_envs, args_cli.num_iterations) + + # Compare computed results + print("\nComparing computed results across implementations...") + comparison_stats = compare_results(all_computed_results, tolerance=1e-4) + print_comparison_results(comparison_stats, tolerance=1e-4) + + # Print profiling instructions if enabled + if args_cli.profile: + print("\n" + "=" * 100) + print("PROFILING RESULTS") + print("=" * 100) + print("Profile files have been saved. To visualize with snakeviz, run:") + for impl_key, profile_file in profile_files.items(): + impl_display = impl_key.replace("_", " ").title() + print(f" # {impl_display}") + print(f" snakeviz {profile_file}") + print("\nAlternatively, use pstats to analyze in terminal:") + print(" python -m pstats ") + print("=" * 100) + print() + + # Clean up + sim_utils.SimulationContext.clear_instance() + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmarks/benchmark_xform_prim_view.py b/scripts/benchmarks/benchmark_xform_prim_view.py new file mode 100644 index 000000000000..94a8dc743610 --- /dev/null +++ b/scripts/benchmarks/benchmark_xform_prim_view.py @@ -0,0 +1,631 @@ +# 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 + +"""Benchmark script comparing XformPrimView implementations across different APIs. + +This script tests the performance of batched transform operations using: +- Isaac Lab's XformPrimView implementation with USD backend +- Isaac Lab's XformPrimView implementation with Fabric backend +- Isaac Sim's XformPrimView implementation (legacy) +- Isaac Sim Experimental's XformPrim implementation (latest) + +Usage: + # Basic benchmark (all APIs) + ./isaaclab.sh -p scripts/benchmarks/benchmark_xform_prim_view.py --num_envs 1024 --device cuda:0 --headless + + # With profiling enabled (for snakeviz visualization) + ./isaaclab.sh -p scripts/benchmarks/benchmark_xform_prim_view.py --num_envs 1024 --profile --headless + + # Then visualize with snakeviz: + snakeviz profile_results/isaaclab_usd_benchmark.prof + snakeviz profile_results/isaaclab_fabric_benchmark.prof + snakeviz profile_results/isaacsim_benchmark.prof + snakeviz profile_results/isaacsim_exp_benchmark.prof +""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# parse the arguments +args_cli = argparse.Namespace() + +parser = argparse.ArgumentParser(description="This script can help you benchmark the performance of XformPrimView.") + +parser.add_argument("--num_envs", type=int, default=100, help="Number of environments to simulate.") +parser.add_argument("--num_iterations", type=int, default=50, help="Number of iterations for each test.") +parser.add_argument( + "--profile", + action="store_true", + help="Enable profiling with cProfile. Results saved as .prof files for snakeviz visualization.", +) +parser.add_argument( + "--profile-dir", + type=str, + default="./profile_results", + help="Directory to save profile results. Default: ./profile_results", +) + +AppLauncher.add_app_launcher_args(parser) +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import cProfile +import time +from typing import Literal + +import torch + +from isaacsim.core.prims import XFormPrim as IsaacSimXformPrimView +from isaacsim.core.utils.extensions import enable_extension + +# compare against latest Isaac Sim implementation +enable_extension("isaacsim.core.experimental.prims") +from isaacsim.core.experimental.prims import XformPrim as IsaacSimExperimentalXformPrimView + +import isaaclab.sim as sim_utils +from isaaclab.sim.views import XformPrimView as IsaacLabXformPrimView + + +@torch.no_grad() +def benchmark_xform_prim_view( # noqa: C901 + api: Literal["isaaclab-usd", "isaaclab-fabric", "isaacsim-usd", "isaacsim-fabric", "isaacsim-exp"], + num_iterations: int, +) -> tuple[dict[str, float], dict[str, torch.Tensor]]: + """Benchmark the Xform view class from Isaac Lab, Isaac Sim, or Isaac Sim Experimental. + + Args: + api: Which API to benchmark: + - "isaaclab-usd": Isaac Lab XformPrimView with USD backend + - "isaaclab-fabric": Isaac Lab XformPrimView with Fabric backend + - "isaacsim-usd": Isaac Sim legacy XformPrimView with USD (usd=True) + - "isaacsim-fabric": Isaac Sim legacy XformPrimView with Fabric (usd=False) + - "isaacsim-exp": Isaac Sim Experimental XformPrim + num_iterations: Number of iterations to run. + + Returns: + A tuple of (timing_results, computed_results) where: + - timing_results: Dictionary containing timing results for various operations + - computed_results: Dictionary containing the computed values for validation + """ + timing_results = {} + computed_results = {} + + # Setup scene + print(" Setting up scene") + # Clear stage + sim_utils.create_new_stage() + # Create simulation context + start_time = time.perf_counter() + sim_cfg = sim_utils.SimulationCfg( + dt=0.01, + device=args_cli.device, + use_fabric=api in ("isaaclab-fabric", "isaacsim-fabric"), + ) + sim = sim_utils.SimulationContext(sim_cfg) + stage = sim_utils.get_current_stage() + + print(f" Time taken to create simulation context: {time.perf_counter() - start_time} seconds") + + # Create prims + prim_paths = [] + for i in range(args_cli.num_envs): + sim_utils.create_prim(f"/World/Env_{i}", "Xform", stage=stage, translation=(i * 2.0, 0.0, 1.0)) + sim_utils.create_prim(f"/World/Env_{i}/Object", "Xform", stage=stage, translation=(0.0, 0.0, 0.0)) + prim_paths.append(f"/World/Env_{i}/Object") + # Play simulation + sim.reset() + + # Pattern to match all prims + pattern = "/World/Env_.*/Object" + print(f" Pattern: {pattern}") + + # Create view + start_time = time.perf_counter() + if api == "isaaclab-usd" or api == "isaaclab-fabric": + xform_view = IsaacLabXformPrimView(pattern, device=args_cli.device, validate_xform_ops=False) + elif api == "isaacsim-usd": + xform_view = IsaacSimXformPrimView(pattern, reset_xform_properties=False, usd=True) + elif api == "isaacsim-fabric": + xform_view = IsaacSimXformPrimView(pattern, reset_xform_properties=False, usd=False) + elif api == "isaacsim-exp": + xform_view = IsaacSimExperimentalXformPrimView(pattern) + else: + raise ValueError(f"Invalid API: {api}") + timing_results["init"] = time.perf_counter() - start_time + + if api in ("isaaclab-usd", "isaaclab-fabric", "isaacsim-usd", "isaacsim-fabric"): + num_prims = xform_view.count + elif api == "isaacsim-exp": + num_prims = len(xform_view.prims) + print(f" XformView managing {num_prims} prims") + + # Benchmark get_world_poses + # Warmup call to initialize Fabric (if needed) - excluded from timing + positions, orientations = xform_view.get_world_poses() + + # Now time the actual iterations (steady-state performance) + start_time = time.perf_counter() + for _ in range(num_iterations): + positions, orientations = xform_view.get_world_poses() + + # Ensure tensors are torch tensors (do this AFTER timing) + if not isinstance(positions, torch.Tensor): + positions = torch.tensor(positions, dtype=torch.float32) + if not isinstance(orientations, torch.Tensor): + orientations = torch.tensor(orientations, dtype=torch.float32) + + timing_results["get_world_poses"] = (time.perf_counter() - start_time) / num_iterations + + # Store initial world poses + computed_results["initial_world_positions"] = positions.clone() + computed_results["initial_world_orientations"] = orientations.clone() + + # Benchmark set_world_poses + new_positions = positions.clone() + new_positions[:, 2] += 0.1 + start_time = time.perf_counter() + for _ in range(num_iterations): + if api in ("isaaclab-usd", "isaaclab-fabric", "isaacsim-usd", "isaacsim-fabric"): + xform_view.set_world_poses(new_positions, orientations) + elif api == "isaacsim-exp": + xform_view.set_world_poses(new_positions.cpu().numpy(), orientations.cpu().numpy()) + timing_results["set_world_poses"] = (time.perf_counter() - start_time) / num_iterations + + # Get world poses after setting to verify + positions_after_set, orientations_after_set = xform_view.get_world_poses() + if not isinstance(positions_after_set, torch.Tensor): + positions_after_set = torch.tensor(positions_after_set, dtype=torch.float32) + if not isinstance(orientations_after_set, torch.Tensor): + orientations_after_set = torch.tensor(orientations_after_set, dtype=torch.float32) + computed_results["world_positions_after_set"] = positions_after_set.clone() + computed_results["world_orientations_after_set"] = orientations_after_set.clone() + + # Benchmark get_local_poses + # Warmup call (though local poses use USD, so minimal overhead) + translations, orientations_local = xform_view.get_local_poses() + + # Now time the actual iterations + start_time = time.perf_counter() + for _ in range(num_iterations): + translations, orientations_local = xform_view.get_local_poses() + # Ensure tensors are torch tensors (do this AFTER timing) + if not isinstance(translations, torch.Tensor): + translations = torch.tensor(translations, dtype=torch.float32, device=args_cli.device) + if not isinstance(orientations_local, torch.Tensor): + orientations_local = torch.tensor(orientations_local, dtype=torch.float32, device=args_cli.device) + + timing_results["get_local_poses"] = (time.perf_counter() - start_time) / num_iterations + + # Store initial local poses + computed_results["initial_local_translations"] = translations.clone() + computed_results["initial_local_orientations"] = orientations_local.clone() + + # Benchmark set_local_poses + new_translations = translations.clone() + new_translations[:, 2] += 0.1 + start_time = time.perf_counter() + for _ in range(num_iterations): + if api in ("isaaclab-usd", "isaaclab-fabric", "isaacsim-usd", "isaacsim-fabric"): + xform_view.set_local_poses(new_translations, orientations_local) + elif api == "isaacsim-exp": + xform_view.set_local_poses(new_translations.cpu().numpy(), orientations_local.cpu().numpy()) + timing_results["set_local_poses"] = (time.perf_counter() - start_time) / num_iterations + + # Get local poses after setting to verify + translations_after_set, orientations_local_after_set = xform_view.get_local_poses() + if not isinstance(translations_after_set, torch.Tensor): + translations_after_set = torch.tensor(translations_after_set, dtype=torch.float32) + if not isinstance(orientations_local_after_set, torch.Tensor): + orientations_local_after_set = torch.tensor(orientations_local_after_set, dtype=torch.float32) + computed_results["local_translations_after_set"] = translations_after_set.clone() + computed_results["local_orientations_after_set"] = orientations_local_after_set.clone() + + # Benchmark combined get operation + # Warmup call (Fabric should already be initialized by now, but for consistency) + positions, orientations = xform_view.get_world_poses() + translations, local_orientations = xform_view.get_local_poses() + + # Now time the actual iterations + start_time = time.perf_counter() + for _ in range(num_iterations): + positions, orientations = xform_view.get_world_poses() + translations, local_orientations = xform_view.get_local_poses() + timing_results["get_both"] = (time.perf_counter() - start_time) / num_iterations + + # Benchmark interleaved set/get (realistic workflow pattern) + # Pre-convert tensors for experimental API to avoid conversion overhead in loop + if api == "isaacsim-exp": + new_positions_np = new_positions.cpu().numpy() + orientations_np = orientations + + # Warmup + if api in ("isaaclab-usd", "isaaclab-fabric", "isaacsim-usd", "isaacsim-fabric"): + xform_view.set_world_poses(new_positions, orientations) + positions, orientations = xform_view.get_world_poses() + elif api == "isaacsim-exp": + xform_view.set_world_poses(new_positions_np, orientations_np) + positions, orientations = xform_view.get_world_poses() + positions = torch.tensor(positions, dtype=torch.float32) + orientations = torch.tensor(orientations, dtype=torch.float32) + + # Now time the actual interleaved iterations + start_time = time.perf_counter() + for _ in range(num_iterations): + # Write then immediately read (common pattern: set pose, verify/query result) + if api in ("isaaclab-usd", "isaaclab-fabric", "isaacsim-usd", "isaacsim-fabric"): + xform_view.set_world_poses(new_positions, orientations) + positions, orientations = xform_view.get_world_poses() + elif api == "isaacsim-exp": + xform_view.set_world_poses(new_positions_np, orientations_np) + positions, orientations = xform_view.get_world_poses() + + timing_results["interleaved_world_set_get"] = (time.perf_counter() - start_time) / num_iterations + + # close simulation + sim.clear() + sim.clear_all_callbacks() + sim.clear_instance() + + return timing_results, computed_results + + +def compare_results( + results_dict: dict[str, dict[str, torch.Tensor]], tolerance: float = 1e-4 +) -> dict[str, dict[str, dict[str, float]]]: + """Compare computed results across multiple implementations. + + Only compares implementations using the same data path: + - USD implementations (isaaclab-usd, isaacsim-usd) are compared with each other + - Fabric implementations (isaaclab-fabric, isaacsim-fabric) are compared with each other + + This is because Fabric is designed for write-first workflows and may not match + USD reads on initialization. + + Args: + results_dict: Dictionary mapping API names to their computed values. + tolerance: Tolerance for numerical comparison. + + Returns: + Nested dictionary: {comparison_pair: {metric: {stats}}}, e.g., + {"isaaclab-usd_vs_isaacsim-usd": {"initial_world_positions": {"max_diff": 0.001, ...}}} + """ + comparison_stats = {} + + # Group APIs by their data path (USD vs Fabric) + usd_apis = [api for api in results_dict.keys() if "usd" in api and "fabric" not in api] + fabric_apis = [api for api in results_dict.keys() if "fabric" in api] + + # Compare within USD group + for i, api1 in enumerate(usd_apis): + for api2 in usd_apis[i + 1 :]: + pair_key = f"{api1}_vs_{api2}" + comparison_stats[pair_key] = {} + + computed1 = results_dict[api1] + computed2 = results_dict[api2] + + for key in computed1.keys(): + if key not in computed2: + print(f" Warning: Key '{key}' not found in {api2} results") + continue + + val1 = computed1[key] + val2 = computed2[key] + + # Compute differences + diff = torch.abs(val1 - val2) + max_diff = torch.max(diff).item() + mean_diff = torch.mean(diff).item() + + # Check if within tolerance + all_close = torch.allclose(val1, val2, atol=tolerance, rtol=0) + + comparison_stats[pair_key][key] = { + "max_diff": max_diff, + "mean_diff": mean_diff, + "all_close": all_close, + } + + # Compare within Fabric group + for i, api1 in enumerate(fabric_apis): + for api2 in fabric_apis[i + 1 :]: + pair_key = f"{api1}_vs_{api2}" + comparison_stats[pair_key] = {} + + computed1 = results_dict[api1] + computed2 = results_dict[api2] + + for key in computed1.keys(): + if key not in computed2: + print(f" Warning: Key '{key}' not found in {api2} results") + continue + + val1 = computed1[key] + val2 = computed2[key] + + # Compute differences + diff = torch.abs(val1 - val2) + max_diff = torch.max(diff).item() + mean_diff = torch.mean(diff).item() + + # Check if within tolerance + all_close = torch.allclose(val1, val2, atol=tolerance, rtol=0) + + comparison_stats[pair_key][key] = { + "max_diff": max_diff, + "mean_diff": mean_diff, + "all_close": all_close, + } + + return comparison_stats + + +def print_comparison_results(comparison_stats: dict[str, dict[str, dict[str, float]]], tolerance: float): + """Print comparison results across implementations. + + Args: + comparison_stats: Nested dictionary containing comparison statistics for each API pair. + tolerance: Tolerance used for comparison. + """ + if not comparison_stats: + print("\n" + "=" * 100) + print("RESULT COMPARISON") + print("=" * 100) + print("ℹ️ No comparisons performed.") + print(" USD and Fabric implementations are not compared because Fabric uses a") + print(" write-first workflow and may not match USD reads on initialization.") + print("=" * 100) + print() + return + + for pair_key, pair_stats in comparison_stats.items(): + # Format the pair key for display (e.g., "isaaclab_vs_isaacsim" -> "Isaac Lab vs Isaac Sim") + api1, api2 = pair_key.split("_vs_") + display_api1 = api1.replace("-", " ").title() + display_api2 = api2.replace("-", " ").title() + comparison_title = f"{display_api1} vs {display_api2}" + + # Check if all results match + all_match = all(stats["all_close"] for stats in pair_stats.values()) + + if all_match: + # Compact output when everything matches + print("\n" + "=" * 100) + print(f"RESULT COMPARISON: {comparison_title}") + print("=" * 100) + print(f"✓ All computed values match within tolerance ({tolerance})") + print("=" * 100) + else: + # Detailed output when there are mismatches + print("\n" + "=" * 100) + print(f"RESULT COMPARISON: {comparison_title}") + print("=" * 100) + print(f"{'Computed Value':<40} {'Max Diff':<15} {'Mean Diff':<15} {'Match':<10}") + print("-" * 100) + + for key, stats in pair_stats.items(): + # Format the key for display + display_key = key.replace("_", " ").title() + match_str = "✓ Yes" if stats["all_close"] else "✗ No" + + print(f"{display_key:<40} {stats['max_diff']:<15.6e} {stats['mean_diff']:<15.6e} {match_str:<10}") + + print("=" * 100) + print(f"\n✗ Some results differ beyond tolerance ({tolerance})") + + # Special note for Isaac Sim Fabric local pose bug + if "isaacsim-fabric" in pair_key and any("local_translations_after_set" in k for k in pair_stats.keys()): + if not pair_stats.get("local_translations_after_set", {}).get("all_close", True): + print("\n ⚠️ Known Issue: Isaac Sim Fabric has a bug where get_local_poses() returns stale") + print(" values after set_local_poses(). Isaac Lab Fabric correctly returns updated values.") + print(" This is a correctness issue in Isaac Sim's implementation, not Isaac Lab's.") + else: + print(f" This may indicate implementation differences between {display_api1} and {display_api2}") + + print() + + +def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num_iterations: int): + """Print benchmark results in a formatted table. + + Args: + results_dict: Dictionary mapping API names to their timing results. + num_prims: Number of prims tested. + num_iterations: Number of iterations run. + """ + print("\n" + "=" * 100) + print(f"BENCHMARK RESULTS: {num_prims} prims, {num_iterations} iterations") + print("=" * 100) + + api_names = list(results_dict.keys()) + # Format API names for display + display_names = [name.replace("-", " ").replace("_", " ").title() for name in api_names] + + # Calculate column width based on number of APIs + col_width = 20 + + # Print header + header = f"{'Operation':<25}" + for display_name in display_names: + header += f" {display_name + ' (ms)':<{col_width}}" + print(header) + print("-" * 100) + + # Print each operation + operations = [ + ("Initialization", "init"), + ("Get World Poses", "get_world_poses"), + ("Set World Poses", "set_world_poses"), + ("Get Local Poses", "get_local_poses"), + ("Set Local Poses", "set_local_poses"), + ("Get Both (World+Local)", "get_both"), + ("Interleaved World Set→Get", "interleaved_world_set_get"), + ] + + for op_name, op_key in operations: + row = f"{op_name:<25}" + for api_name in api_names: + api_time = results_dict[api_name].get(op_key, 0) * 1000 # Convert to ms + row += f" {api_time:>{col_width - 1}.4f}" + print(row) + + print("=" * 100) + + # Calculate and print total time + total_row = f"{'Total Time':<25}" + for api_name in api_names: + total_time = sum(results_dict[api_name].values()) * 1000 + total_row += f" {total_time:>{col_width - 1}.4f}" + print(f"\n{total_row}") + + # Calculate speedups relative to Isaac Lab USD (baseline) + if "isaaclab-usd" in api_names: + print("\n" + "=" * 100) + print("SPEEDUP vs Isaac Lab USD (Baseline)") + print("=" * 100) + print(f"{'Operation':<25}", end="") + for api_name, display_name in zip(api_names, display_names): + if api_name != "isaaclab-usd": + print(f" {display_name:<{col_width}}", end="") + print() + print("-" * 100) + + isaaclab_usd_results = results_dict["isaaclab-usd"] + for op_name, op_key in operations: + print(f"{op_name:<25}", end="") + isaaclab_usd_time = isaaclab_usd_results.get(op_key, 0) + for api_name, display_name in zip(api_names, display_names): + if api_name != "isaaclab-usd": + api_time = results_dict[api_name].get(op_key, 0) + if isaaclab_usd_time > 0 and api_time > 0: + speedup = isaaclab_usd_time / api_time + print(f" {speedup:>{col_width - 1}.2f}x", end="") + else: + print(f" {'N/A':>{col_width}}", end="") + print() + + # Overall speedup + print("=" * 100) + print(f"{'Overall Speedup':<25}", end="") + total_isaaclab_usd = sum(isaaclab_usd_results.values()) + for api_name, display_name in zip(api_names, display_names): + if api_name != "isaaclab-usd": + total_api = sum(results_dict[api_name].values()) + if total_isaaclab_usd > 0 and total_api > 0: + overall_speedup = total_isaaclab_usd / total_api + print(f" {overall_speedup:>{col_width - 1}.2f}x", end="") + else: + print(f" {'N/A':>{col_width}}", end="") + print() + + print("\n" + "=" * 100) + print("\nNotes:") + print(" - Times are averaged over all iterations") + print(" - Speedup = (Isaac Lab USD time) / (Other API time)") + print(" - Speedup > 1.0 means the other API is faster than Isaac Lab USD") + print(" - Speedup < 1.0 means the other API is slower than Isaac Lab USD") + print() + + +def main(): + """Main benchmark function.""" + print("=" * 100) + print("XformPrimView Benchmark - Comparing Multiple APIs") + print("=" * 100) + print("Configuration:") + print(f" Number of environments: {args_cli.num_envs}") + print(f" Iterations per test: {args_cli.num_iterations}") + print(f" Device: {args_cli.device}") + print(f" Profiling: {'Enabled' if args_cli.profile else 'Disabled'}") + if args_cli.profile: + print(f" Profile directory: {args_cli.profile_dir}") + print() + + # Create profile directory if profiling is enabled + if args_cli.profile: + import os + + os.makedirs(args_cli.profile_dir, exist_ok=True) + + # Dictionary to store all results + all_timing_results = {} + all_computed_results = {} + profile_files = {} + + # APIs to benchmark + apis_to_test = [ + ("isaaclab-usd", "Isaac Lab XformPrimView (USD)"), + ("isaaclab-fabric", "Isaac Lab XformPrimView (Fabric)"), + ("isaacsim-usd", "Isaac Sim XformPrimView (USD)"), + ("isaacsim-fabric", "Isaac Sim XformPrimView (Fabric)"), + ("isaacsim-exp", "Isaac Sim Experimental XformPrim"), + ] + + # Benchmark each API + for api_key, api_name in apis_to_test: + print(f"Benchmarking {api_name}...") + + if args_cli.profile: + profiler = cProfile.Profile() + profiler.enable() + + # Cast api_key to Literal type for type checker + timing, computed = benchmark_xform_prim_view( + api=api_key, # type: ignore[arg-type] + num_iterations=args_cli.num_iterations, + ) + + if args_cli.profile: + profiler.disable() + profile_file = f"{args_cli.profile_dir}/{api_key.replace('-', '_')}_benchmark.prof" + profiler.dump_stats(profile_file) + profile_files[api_key] = profile_file + print(f" Profile saved to: {profile_file}") + + all_timing_results[api_key] = timing + all_computed_results[api_key] = computed + + print(" Done!") + print() + + # Print timing results + print_results(all_timing_results, args_cli.num_envs, args_cli.num_iterations) + + # Compare computed results + print("\nComparing computed results across APIs...") + comparison_stats = compare_results(all_computed_results, tolerance=1e-6) + print_comparison_results(comparison_stats, tolerance=1e-4) + + # Print profiling instructions if enabled + if args_cli.profile: + print("\n" + "=" * 100) + print("PROFILING RESULTS") + print("=" * 100) + print("Profile files have been saved. To visualize with snakeviz, run:") + for api_key, profile_file in profile_files.items(): + api_display = api_key.replace("-", " ").title() + print(f" # {api_display}") + print(f" snakeviz {profile_file}") + print("\nAlternatively, use pstats to analyze in terminal:") + print(" python -m pstats ") + print("=" * 100) + print() + + # Clean up + sim_utils.SimulationContext.clear_instance() + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmarks/utils.py b/scripts/benchmarks/utils.py index ff2ca5c0114b..8401320f4e50 100644 --- a/scripts/benchmarks/utils.py +++ b/scripts/benchmarks/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +7,10 @@ import glob import os +from tensorboard.backend.event_processing import event_accumulator + from isaacsim.benchmark.services import BaseIsaacBenchmark from isaacsim.benchmark.services.metrics.measurements import DictMeasurement, ListMeasurement, SingleMeasurement -from tensorboard.backend.event_processing import event_accumulator def parse_tf_logs(log_dir: str): diff --git a/scripts/demos/arms.py b/scripts/demos/arms.py index ac050d8fe65c..92bd4499d6d5 100644 --- a/scripts/demos/arms.py +++ b/scripts/demos/arms.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,8 +35,6 @@ import numpy as np import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.assets import Articulation from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -86,7 +84,7 @@ def design_scene() -> tuple[dict, list[list[float]]]: origins = define_origins(num_origins=6, spacing=2.0) # Origin 1 with Franka Panda - prim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) + sim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) # -- Table cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd") cfg.func("/World/Origin1/Table", cfg, translation=(0.55, 0.0, 1.05)) @@ -96,7 +94,7 @@ def design_scene() -> tuple[dict, list[list[float]]]: franka_panda = Articulation(cfg=franka_arm_cfg) # Origin 2 with UR10 - prim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) + sim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) # -- Table cfg = sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/Stand/stand_instanceable.usd", scale=(2.0, 2.0, 2.0) @@ -108,7 +106,7 @@ def design_scene() -> tuple[dict, list[list[float]]]: ur10 = Articulation(cfg=ur10_cfg) # Origin 3 with Kinova JACO2 (7-Dof) arm - prim_utils.create_prim("/World/Origin3", "Xform", translation=origins[2]) + sim_utils.create_prim("/World/Origin3", "Xform", translation=origins[2]) # -- Table cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/ThorlabsTable/table_instanceable.usd") cfg.func("/World/Origin3/Table", cfg, translation=(0.0, 0.0, 0.8)) @@ -118,7 +116,7 @@ def design_scene() -> tuple[dict, list[list[float]]]: kinova_j2n7s300 = Articulation(cfg=kinova_arm_cfg) # Origin 4 with Kinova JACO2 (6-Dof) arm - prim_utils.create_prim("/World/Origin4", "Xform", translation=origins[3]) + sim_utils.create_prim("/World/Origin4", "Xform", translation=origins[3]) # -- Table cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/ThorlabsTable/table_instanceable.usd") cfg.func("/World/Origin4/Table", cfg, translation=(0.0, 0.0, 0.8)) @@ -128,7 +126,7 @@ def design_scene() -> tuple[dict, list[list[float]]]: kinova_j2n6s300 = Articulation(cfg=kinova_arm_cfg) # Origin 5 with Sawyer - prim_utils.create_prim("/World/Origin5", "Xform", translation=origins[4]) + sim_utils.create_prim("/World/Origin5", "Xform", translation=origins[4]) # -- Table cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd") cfg.func("/World/Origin5/Table", cfg, translation=(0.55, 0.0, 1.05)) @@ -138,7 +136,7 @@ def design_scene() -> tuple[dict, list[list[float]]]: kinova_gen3n7 = Articulation(cfg=kinova_arm_cfg) # Origin 6 with Kinova Gen3 (7-Dof) arm - prim_utils.create_prim("/World/Origin6", "Xform", translation=origins[5]) + sim_utils.create_prim("/World/Origin6", "Xform", translation=origins[5]) # -- Table cfg = sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/Stand/stand_instanceable.usd", scale=(2.0, 2.0, 2.0) diff --git a/scripts/demos/bin_packing.py b/scripts/demos/bin_packing.py new file mode 100644 index 000000000000..a43cbf199b25 --- /dev/null +++ b/scripts/demos/bin_packing.py @@ -0,0 +1,354 @@ +# 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 + +"""Demonstration of randomized bin-packing with Isaac Lab. + +This script tiles multiple environments, spawns a configurable set of grocery +objects, and continuously randomizes their poses, velocities, mass properties, +and active/cached state to mimic a bin filling workflow. It showcases how to +use ``RigidObjectCollection`` utilities for bulk pose resets, cache management, +and out-of-bounds recovery inside an interactive simulation loop. + +.. code-block:: bash + + # Usage + ./isaaclab.sh -p scripts/demos/bin_packing.py --num_envs 32 + +""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Demo usage of RigidObjectCollection through bin packing example") +parser.add_argument("--num_envs", type=int, default=16, help="Number of environments to spawn.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import math + +import torch + +import isaaclab.sim as sim_utils +import isaaclab.utils.math as math_utils +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg, RigidObjectCollection, RigidObjectCollectionCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.sim import SimulationContext +from isaaclab.utils import Timer, configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +## +# Scene Configuration +## + +# Layout and spawn counts. +MAX_NUM_OBJECTS = 24 # Hard cap on objects managed per environment (active + cached). +MAX_OBJECTS_PER_BIN = 24 # Maximum active objects we plan to fit inside the bin. +MIN_OBJECTS_PER_BIN = 1 # Lower bound for randomized active object count. +NUM_OBJECTS_PER_LAYER = 4 # Number of groceries spawned on each layer of the active stack. + +# Cached staging area and grid spacing. +CACHE_HEIGHT = 2.5 # Height (m) at which inactive groceries wait out of view. +ACTIVE_LAYER_SPACING = 0.1 # Vertical spacing (m) between layers inside the bin. +CACHE_SPACING = 0.25 # XY spacing (m) between cached groceries. + +# Bin dimensions and bounds. +BIN_DIMENSIONS = (0.2, 0.3, 0.15) # Physical size (m) of the storage bin. +BIN_XY_BOUND = ((-0.2, -0.3), (0.2, 0.3)) # Valid XY region (min/max) for active groceries. + +# Randomization ranges (radians for rotations, m/s and rad/s for velocities). +POSE_RANGE = {"roll": (-3.14, 3.14), "pitch": (-3.14, 3.14), "yaw": (-3.14, 3.14)} +VELOCITY_RANGE = {"roll": (-0.2, 1.0), "pitch": (-0.2, 1.0), "yaw": (-0.2, 1.0)} + +# Object layout configuration + +GROCERIES = { + "OBJECT_A": sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/004_sugar_box.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4), + ), + "OBJECT_B": sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/003_cracker_box.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4), + ), + "OBJECT_C": sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/005_tomato_soup_can.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4), + ), + "OBJECT_D": sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/YCB/Axis_Aligned_Physics/006_mustard_bottle.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(solver_position_iteration_count=4), + ), +} + + +@configclass +class MultiObjectSceneCfg(InteractiveSceneCfg): + """Configuration for a multi-object scene.""" + + # ground plane + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # rigid object + object: RigidObjectCfg = RigidObjectCfg( + prim_path="/World/envs/env_.*/Object", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/KLT_Bin/small_KLT.usd", + scale=(2.0, 2.0, 2.0), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0, kinematic_enabled=True + ), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.15)), + ) + + groceries: RigidObjectCollectionCfg = RigidObjectCollectionCfg( + # Instantiate four grocery variants per layer and replicate across all layers in each environment. + rigid_objects={ + f"Object_{label}_Layer{layer}": RigidObjectCfg( + prim_path=f"/World/envs/env_.*/Object_{label}_Layer{layer}", + init_state=RigidObjectCfg.InitialStateCfg(pos=(x, y, 0.2 + (layer) * 0.2)), + spawn=GROCERIES.get(f"OBJECT_{label}"), + ) + for layer in range(MAX_NUM_OBJECTS // NUM_OBJECTS_PER_LAYER) + for label, (x, y) in zip(["A", "B", "C", "D"], [(-0.035, -0.1), (-0.035, 0.1), (0.035, 0.1), (0.035, -0.1)]) + } + ) + + +def reset_object_collections( + scene: InteractiveScene, asset_name: str, view_states: torch.Tensor, view_ids: torch.Tensor, noise: bool = False +) -> None: + """Apply states to a subset of a collection, with optional noise. + + Updates ``view_states`` in-place for ``view_ids`` and writes transforms/velocities + to the PhysX view for the collection ``asset_name``. When ``noise`` is True, adds + uniform perturbations to pose (XYZ + Euler) and velocities using ``POSE_RANGE`` and + ``VELOCITY_RANGE``. + + Args: + scene: Interactive scene containing the collection. + asset_name: Key in the scene (e.g., ``"groceries"``) for the RigidObjectCollection. + view_states: Flat tensor (N, 13) with [x, y, z, qx, qy, qz, qw, lin(3), ang(3)] in world frame. + view_ids: 1D tensor of indices into ``view_states`` to update. + noise: If True, apply pose and velocity noise before writing. + + Returns: + None: This function updates ``view_states`` and the underlying PhysX view in-place. + """ + rigid_object_collection: RigidObjectCollection = scene[asset_name] + sel_view_states = view_states[view_ids] + positions = sel_view_states[:, :3] + orientations = sel_view_states[:, 3:7] + # poses + if noise: + range_list = [POSE_RANGE.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=scene.device) + samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(view_ids), 6), device=scene.device) + positions += samples[..., 0:3] + + # Compose new orientations by applying the sampled euler noise in quaternion space. + orientations_delta = math_utils.quat_from_euler_xyz(samples[..., 3], samples[..., 4], samples[..., 5]) + orientations = math_utils.convert_quat(orientations, to="wxyz") + orientations = math_utils.quat_mul(orientations, orientations_delta) + orientations = math_utils.convert_quat(orientations, to="xyzw") + + # velocities + new_velocities = sel_view_states[:, 7:13] + if noise: + range_list = [VELOCITY_RANGE.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=scene.device) + samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(view_ids), 6), device=scene.device) + new_velocities += samples + else: + new_velocities[:] = 0.0 + + view_states[view_ids, :7] = torch.concat((positions, orientations), dim=-1) + view_states[view_ids, 7:] = new_velocities + + rigid_object_collection.root_physx_view.set_transforms(view_states[:, :7], indices=view_ids) + rigid_object_collection.root_physx_view.set_velocities(view_states[:, 7:], indices=view_ids) + + +def build_grocery_defaults( + num_envs: int, + device: str = "cpu", +) -> tuple[torch.Tensor, torch.Tensor]: + """Create default active/cached spawn poses for all environments. + + - Active poses: stacked 3D grid over the bin with ``ACTIVE_LAYER_SPACING`` per layer. + - Cached poses: 2D grid at ``CACHE_HEIGHT`` to park inactive objects out of view. + + Args: + num_envs: Number of environments to tile the poses for. + device: Torch device for allocation (e.g., ``"cuda:0"`` or ``"cpu"``). + + Returns: + tuple[torch.Tensor, torch.Tensor]: Active and cached spawn poses, each shaped + ``(num_envs, M, 7)`` with ``[x, y, z, qx, qy, qz, qw]`` where ``M`` equals + ``MAX_NUM_OBJECTS``. + """ + + # The bin has a size of 0.2 x 0.3 x 0.15 m + bin_x_dim, bin_y_dim, bin_z_dim = BIN_DIMENSIONS + # First, we calculate the number of layers and objects per layer + num_layers = math.ceil(MAX_OBJECTS_PER_BIN / NUM_OBJECTS_PER_LAYER) + num_x_objects = math.ceil(math.sqrt(NUM_OBJECTS_PER_LAYER)) + num_y_objects = math.ceil(NUM_OBJECTS_PER_LAYER / num_x_objects) + total_objects = num_x_objects * num_y_objects * num_layers + # Then, we create a 3D grid that allows for IxJxN objects to be placed on top of the bin. + x = torch.linspace(-bin_x_dim * (2 / 6), bin_x_dim * (2 / 6), num_x_objects, device=device) + y = torch.linspace(-bin_y_dim * (2 / 6), bin_y_dim * (2 / 6), num_y_objects, device=device) + z = torch.linspace(0, ACTIVE_LAYER_SPACING * (num_layers - 1), num_layers, device=device) + bin_z_dim * 2 + grid_z, grid_y, grid_x = torch.meshgrid(z, y, x, indexing="ij") # Note Z first, this stacks the layers. + # Using this grid plus a reference quaternion, create the poses for the groceries to be spawned above the bin. + ref_quat = torch.tensor([[0.0, 0.0, 0.0, 1.0]], device=device).repeat(total_objects, 1) + positions = torch.stack((grid_x.flatten(), grid_y.flatten(), grid_z.flatten()), dim=-1) + poses = torch.cat((positions, ref_quat), dim=-1) + # Duplicate across environments, cap at max_num_objects + active_spawn_poses = poses.unsqueeze(0).repeat(num_envs, 1, 1)[:, :MAX_NUM_OBJECTS, :] + + # We'll also create a buffer for the cached groceries. They'll be spawned below the bin so they can't be seen. + num_x_objects = math.ceil(math.sqrt(MAX_NUM_OBJECTS)) + num_y_objects = math.ceil(MAX_NUM_OBJECTS / num_x_objects) + # We create a XY grid only and fix the Z height for the cache. + x = CACHE_SPACING * torch.arange(num_x_objects, device=device) + y = CACHE_SPACING * torch.arange(num_y_objects, device=device) + grid_y, grid_x = torch.meshgrid(y, x, indexing="ij") + grid_z = CACHE_HEIGHT * torch.ones_like(grid_x) + # We can then create the poses for the cached groceries. + ref_quat = torch.tensor([[1.0, 0.0, 0.0, 0.0]], device=device).repeat(num_x_objects * num_y_objects, 1) + positions = torch.stack((grid_x.flatten(), grid_y.flatten(), grid_z.flatten()), dim=-1) + poses = torch.cat((positions, ref_quat), dim=-1) + # Duplicate across environments, cap at max_num_objects + cached_spawn_poses = poses.unsqueeze(0).repeat(num_envs, 1, 1)[:, :MAX_NUM_OBJECTS, :] + + return active_spawn_poses, cached_spawn_poses + + +## +# Simulation Loop +## + + +def run_simulator(sim: SimulationContext, scene: InteractiveScene) -> None: + """Runs the simulation loop that coordinates spawn randomization and stepping. + + Returns: + None: The simulator side-effects are applied through ``scene`` and ``sim``. + """ + # Extract scene entities + # note: we only do this here for readability. + groceries: RigidObjectCollection = scene["groceries"] + num_objects = groceries.num_objects + num_envs = scene.num_envs + device = scene.device + view_indices = torch.arange(num_envs * num_objects, device=device) + default_state_w = groceries.data.default_object_state.clone() + default_state_w[..., :3] = default_state_w[..., :3] + scene.env_origins.unsqueeze(1) + # Define simulation stepping + sim_dt = sim.get_physics_dt() + count = 0 + + # Pre-compute canonical spawn poses for each object both inside the bin and in the cache. + active_spawn_poses, cached_spawn_poses = build_grocery_defaults(num_envs, device) + # Offset poses into each environment's world frame. + active_spawn_poses[..., :3] += scene.env_origins.view(-1, 1, 3) + cached_spawn_poses[..., :3] += scene.env_origins.view(-1, 1, 3) + active_spawn_poses = groceries.reshape_data_to_view(active_spawn_poses) + cached_spawn_poses = groceries.reshape_data_to_view(cached_spawn_poses) + spawn_w = groceries.reshape_data_to_view(default_state_w).clone() + + groceries_mask_helper = torch.arange(num_objects * num_envs, device=device) % num_objects + # Precompute a helper mask to toggle objects between active and cached sets. + # Precompute XY bounds [[x_min,y_min],[x_max,y_max]] + bounds_xy = torch.as_tensor(BIN_XY_BOUND, device=device, dtype=spawn_w.dtype) + # Simulation loop + while simulation_app.is_running(): + # Reset + if count % 250 == 0: + # reset counter + count = 0 + # Randomly choose how many groceries stay active in each environment. + num_active_groceries = torch.randint(MIN_OBJECTS_PER_BIN, num_objects, (num_envs, 1), device=device) + groceries_mask = (groceries_mask_helper.view(num_envs, -1) < num_active_groceries).view(-1, 1) + spawn_w[:, :7] = cached_spawn_poses * (~groceries_mask) + active_spawn_poses * groceries_mask + # Retrieve positions + with Timer("[INFO] Time to reset scene: "): + reset_object_collections(scene, "groceries", spawn_w, view_indices[~groceries_mask.view(-1)]) + reset_object_collections(scene, "groceries", spawn_w, view_indices[groceries_mask.view(-1)], noise=True) + # Vary the mass and gravity settings so cached objects stay parked. + random_masses = torch.rand(groceries.num_instances * num_objects, device=device) * 0.2 + 0.2 + groceries.root_physx_view.set_masses(random_masses.cpu(), view_indices.cpu()) + groceries.root_physx_view.set_disable_gravities((~groceries_mask).cpu(), indices=view_indices.cpu()) + scene.reset() + + # Write data to sim + scene.write_data_to_sim() + # Perform step + sim.step() + + # Bring out-of-bounds objects back to the bin in one pass. + xy = groceries.reshape_data_to_view(groceries.data.object_pos_w - scene.env_origins.unsqueeze(1))[:, :2] + out_bound = torch.nonzero(~((xy >= bounds_xy[0]) & (xy <= bounds_xy[1])).all(dim=1), as_tuple=False).flatten() + if out_bound.numel(): + # Teleport stray objects back into the active stack to keep the bin tidy. + reset_object_collections(scene, "groceries", spawn_w, out_bound) + # Increment counter + count += 1 + # Update buffers + scene.update(sim_dt) + + +def main() -> None: + """Main function. + + Returns: + None: The function drives the simulation for its side-effects. + """ + # Load kit helper + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view((2.5, 0.0, 4.0), (0.0, 0.0, 2.0)) + + # Design scene + scene_cfg = MultiObjectSceneCfg(num_envs=args_cli.num_envs, env_spacing=1.0, replicate_physics=False) + with Timer("[INFO] Time to create scene: "): + scene = InteractiveScene(scene_cfg) + + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim, scene) + + +if __name__ == "__main__": + # run the main execution + main() + # close sim app + simulation_app.close() diff --git a/scripts/demos/bipeds.py b/scripts/demos/bipeds.py index 0a3852511988..91421c105ff6 100644 --- a/scripts/demos/bipeds.py +++ b/scripts/demos/bipeds.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -56,11 +56,13 @@ def design_scene(sim: sim_utils.SimulationContext) -> tuple[list, torch.Tensor]: cfg.func("/World/Light", cfg) # Define origins - origins = torch.tensor([ - [0.0, -1.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - ]).to(device=sim.device) + origins = torch.tensor( + [ + [0.0, -1.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + ] + ).to(device=sim.device) # Robots cassie = Articulation(CASSIE_CFG.replace(prim_path="/World/Cassie")) diff --git a/scripts/demos/deformables.py b/scripts/demos/deformables.py index 1ff1c78ea113..9b9a962c26d5 100644 --- a/scripts/demos/deformables.py +++ b/scripts/demos/deformables.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +31,9 @@ """Rest everything follows.""" -import numpy as np import random + +import numpy as np import torch import tqdm diff --git a/scripts/demos/h1_locomotion.py b/scripts/demos/h1_locomotion.py index 4f1ed0aabfbd..734ca506a46f 100644 --- a/scripts/demos/h1_locomotion.py +++ b/scripts/demos/h1_locomotion.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -41,21 +41,22 @@ simulation_app = app_launcher.app """Rest everything follows.""" + import torch +from rsl_rl.runners import OnPolicyRunner import carb import omni -from isaacsim.core.utils.stage import get_current_stage from omni.kit.viewport.utility import get_viewport_from_window_name from omni.kit.viewport.utility.camera_state import ViewportCameraState from pxr import Gf, Sdf -from rsl_rl.runners import OnPolicyRunner from isaaclab.envs import ManagerBasedRLEnv +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.utils.math import quat_apply -from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlVecEnvWrapper +from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint from isaaclab_tasks.manager_based.locomotion.velocity.config.h1.rough_env_cfg import H1RoughEnvCfg_PLAY @@ -145,7 +146,7 @@ def _on_keyboard_event(self, event): if event.type == carb.input.KeyboardEventType.KEY_PRESS: # Arrow keys map to pre-defined command vectors to control navigation of robot if event.input.name in self._key_to_control: - if self._selected_id: + if self._selected_id is not None: self.commands[self._selected_id] = self._key_to_control[event.input.name] # Escape key exits out of the current selected robot view elif event.input.name == "ESCAPE": @@ -159,7 +160,7 @@ def _on_keyboard_event(self, event): self.viewport.set_active_camera(self.camera_path) # On key release, the robot stops moving elif event.type == carb.input.KeyboardEventType.KEY_RELEASE: - if self._selected_id: + if self._selected_id is not None: self.commands[self._selected_id] = self._key_to_control["ZEROS"] def update_selected_object(self): diff --git a/scripts/demos/hands.py b/scripts/demos/hands.py index db5bddef23a4..a0fa04e0fbfd 100644 --- a/scripts/demos/hands.py +++ b/scripts/demos/hands.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,8 +35,6 @@ import numpy as np import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.assets import Articulation @@ -76,12 +74,12 @@ def design_scene() -> tuple[dict, list[list[float]]]: origins = define_origins(num_origins=2, spacing=0.5) # Origin 1 with Allegro Hand - prim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) + sim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) # -- Robot allegro = Articulation(ALLEGRO_HAND_CFG.replace(prim_path="/World/Origin1/Robot")) # Origin 2 with Shadow Hand - prim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) + sim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) # -- Robot shadow_hand = Articulation(SHADOW_HAND_CFG.replace(prim_path="/World/Origin2/Robot")) diff --git a/scripts/demos/haply_teleoperation.py b/scripts/demos/haply_teleoperation.py new file mode 100644 index 000000000000..b6d02900baf4 --- /dev/null +++ b/scripts/demos/haply_teleoperation.py @@ -0,0 +1,361 @@ +# 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 + +""" +Demonstration of Haply device teleoperation with a robotic arm. + +This script demonstrates how to use a Haply device (Inverse3 + VerseGrip) to +teleoperate a robotic arm in Isaac Lab. The Haply provides: +- Position tracking from the Inverse3 device +- Orientation and button inputs from the VerseGrip device +- Force feedback + +.. code-block:: bash + + # Usage + ./isaaclab.sh -p scripts/demos/haply_teleoperation.py + + # With custom WebSocket URI + ./isaaclab.sh -p scripts/demos/haply_teleoperation.py --websocket_uri ws://localhost:10001 + + # With sensitivity adjustment + ./isaaclab.sh -p scripts/demos/haply_teleoperation.py --pos_sensitivity 2.0 + +Prerequisites: + 1. Install websockets package: pip install websockets + 2. Have Haply SDK running and accessible via WebSocket + 3. Connect Inverse3 and VerseGrip devices +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Demonstration of Haply device teleoperation with Isaac Lab.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to spawn.") +parser.add_argument( + "--websocket_uri", + type=str, + default="ws://localhost:10001", + help="WebSocket URI for Haply SDK connection.", +) +parser.add_argument( + "--pos_sensitivity", + type=float, + default=1.0, + help="Position sensitivity scaling factor.", +) + +AppLauncher.add_app_launcher_args(parser) +args_cli = parser.parse_args() +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +import numpy as np +import torch + +import isaaclab.sim as sim_utils +from isaaclab.assets import Articulation, AssetBaseCfg, RigidObject, RigidObjectCfg +from isaaclab.controllers import DifferentialIKController, DifferentialIKControllerCfg +from isaaclab.devices import HaplyDevice, HaplyDeviceCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.sensors import ContactSensor, ContactSensorCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from isaaclab_assets import FRANKA_PANDA_HIGH_PD_CFG # isort: skip + +# Workspace mapping constants +HAPLY_Z_OFFSET = 0.35 +WORKSPACE_LIMITS = { + "x": (0.1, 0.9), + "y": (-0.50, 0.50), + "z": (1.05, 1.85), +} + + +def apply_haply_to_robot_mapping( + haply_pos: np.ndarray | torch.Tensor, + haply_initial_pos: np.ndarray | list, + robot_initial_pos: np.ndarray | torch.Tensor, +) -> np.ndarray: + """Apply coordinate mapping from Haply workspace to Franka Panda end-effector. + + Uses absolute position control: robot position = robot_initial_pos + haply_pos (transformed) + + Args: + haply_pos: Current Haply absolute position [x, y, z] in meters + haply_initial_pos: Haply's zero reference position [x, y, z] + robot_initial_pos: Base offset for robot end-effector + + Returns: + robot_pos: Target position for robot EE in world frame [x, y, z] + + """ + # Convert to numpy + if isinstance(haply_pos, torch.Tensor): + haply_pos = haply_pos.cpu().numpy() + if isinstance(robot_initial_pos, torch.Tensor): + robot_initial_pos = robot_initial_pos.cpu().numpy() + + haply_delta = haply_pos - haply_initial_pos + + # Coordinate system mapping: Haply (X, Y, Z) -> Robot (-Y, X, Z-offset) + robot_offset = np.array([-haply_delta[1], haply_delta[0], haply_delta[2] - HAPLY_Z_OFFSET]) + robot_pos = robot_initial_pos + robot_offset + + # Apply workspace limits for safety + robot_pos[0] = np.clip(robot_pos[0], WORKSPACE_LIMITS["x"][0], WORKSPACE_LIMITS["x"][1]) + robot_pos[1] = np.clip(robot_pos[1], WORKSPACE_LIMITS["y"][0], WORKSPACE_LIMITS["y"][1]) + robot_pos[2] = np.clip(robot_pos[2], WORKSPACE_LIMITS["z"][0], WORKSPACE_LIMITS["z"][1]) + + return robot_pos + + +@configclass +class FrankaHaplySceneCfg(InteractiveSceneCfg): + """Configuration for Franka scene with Haply teleoperation and contact sensors.""" + + ground = AssetBaseCfg( + prim_path="/World/defaultGroundPlane", + spawn=sim_utils.GroundPlaneCfg(), + ) + + dome_light = AssetBaseCfg( + prim_path="/World/Light", + spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)), + ) + + table = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Table", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd", + scale=(1.0, 1.0, 1.0), + ), + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.50, 0.0, 1.05), rot=(0.707, 0, 0, 0.707)), + ) + + robot: Articulation = FRANKA_PANDA_HIGH_PD_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + robot.init_state.pos = (-0.02, 0.0, 1.05) + robot.spawn.activate_contact_sensors = True + + cube = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube", + spawn=sim_utils.CuboidCfg( + size=(0.06, 0.06, 0.06), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=0.5, dynamic_friction=0.5), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.8, 0.2), metallic=0.2), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.60, 0.00, 1.15)), + ) + + left_finger_contact_sensor = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_leftfinger", + update_period=0.0, + history_length=3, + debug_vis=True, + track_pose=True, + ) + + right_finger_contact_sensor = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/panda_rightfinger", + update_period=0.0, + history_length=3, + debug_vis=True, + track_pose=True, + ) + + +def run_simulator( + sim: sim_utils.SimulationContext, + scene: InteractiveScene, + haply_device: HaplyDevice, +): + """Runs the simulation loop with Haply teleoperation.""" + sim_dt = sim.get_physics_dt() + count = 1 + + robot: Articulation = scene["robot"] + cube: RigidObject = scene["cube"] + left_finger_sensor: ContactSensor = scene["left_finger_contact_sensor"] + right_finger_sensor: ContactSensor = scene["right_finger_contact_sensor"] + + ee_body_name = "panda_hand" + ee_body_idx = robot.body_names.index(ee_body_name) + + joint_pos = robot.data.default_joint_pos.clone() + joint_pos[0, :7] = torch.tensor([0.0, -0.569, 0.0, -2.81, 0.0, 3.037, 0.741], device=robot.device) + joint_vel = robot.data.default_joint_vel.clone() + robot.write_joint_state_to_sim(joint_pos, joint_vel) + + for _ in range(10): + scene.write_data_to_sim() + sim.step() + scene.update(sim_dt) + + # Initialize the position of franka + robot_initial_pos = robot.data.body_pos_w[0, ee_body_idx].cpu().numpy() + haply_initial_pos = np.array([0.0, 0.0, 0.0], dtype=np.float32) + + ik_controller_cfg = DifferentialIKControllerCfg( + command_type="position", + use_relative_mode=False, + ik_method="dls", + ik_params={"lambda_val": 0.05}, + ) + + # IK joints control arms, buttons control ee rotation and gripper open/close + arm_joint_names = [ + "panda_joint1", + "panda_joint2", + "panda_joint3", + "panda_joint4", + "panda_joint5", + "panda_joint6", + ] + arm_joint_indices = [robot.joint_names.index(name) for name in arm_joint_names] + + # Initialize IK controller + ik_controller = DifferentialIKController(cfg=ik_controller_cfg, num_envs=scene.num_envs, device=sim.device) + initial_ee_quat = robot.data.body_quat_w[:, ee_body_idx] + ik_controller.set_command(command=torch.zeros(scene.num_envs, 3, device=sim.device), ee_quat=initial_ee_quat) + + prev_button_a = False + prev_button_b = False + prev_button_c = False + gripper_target = 0.04 + + # Initialize the rotation of franka end-effector + ee_rotation_angle = robot.data.joint_pos[0, 6].item() + rotation_step = np.pi / 3 + + print("\n[INFO] Teleoperation ready!") + print(" Move handler: Control pose of the end-effector") + print(" Button A: Open | Button B: Close | Button C: Rotate EE (60°)\n") + + while simulation_app.is_running(): + if count % 10000 == 0: + count = 1 + 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:]) + + joint_pos = robot.data.default_joint_pos.clone() + joint_pos[0, :7] = torch.tensor([0.0, -0.569, 0.0, -2.81, 0.0, 3.037, 0.741], device=robot.device) + joint_vel = robot.data.default_joint_vel.clone() + robot.write_joint_state_to_sim(joint_pos, joint_vel) + + cube_state = cube.data.default_root_state.clone() + cube_state[:, :3] += scene.env_origins + cube.write_root_pose_to_sim(cube_state[:, :7]) + cube.write_root_velocity_to_sim(cube_state[:, 7:]) + + scene.reset() + haply_device.reset() + ik_controller.reset() + print("[INFO]: Resetting robot state...") + + # Get the data from Haply device + haply_data = haply_device.advance() + + haply_pos = haply_data[:3] + button_a = haply_data[7].item() > 0.5 + button_b = haply_data[8].item() > 0.5 + button_c = haply_data[9].item() > 0.5 + + if button_a and not prev_button_a: + gripper_target = 0.04 # Open gripper + if button_b and not prev_button_b: + gripper_target = 0.0 # Close gripper + if button_c and not prev_button_c: + joint_7_limit = 3.0 + ee_rotation_angle += rotation_step + + if ee_rotation_angle > joint_7_limit: + ee_rotation_angle = -joint_7_limit + (ee_rotation_angle - joint_7_limit) + elif ee_rotation_angle < -joint_7_limit: + ee_rotation_angle = joint_7_limit + (ee_rotation_angle + joint_7_limit) + + prev_button_a = button_a + prev_button_b = button_b + prev_button_c = button_c + + # Compute IK + target_pos = apply_haply_to_robot_mapping( + haply_pos, + haply_initial_pos, + robot_initial_pos, + ) + + target_pos_tensor = torch.tensor(target_pos, dtype=torch.float32, device=sim.device).unsqueeze(0) + + current_joint_pos = robot.data.joint_pos[:, arm_joint_indices] + ee_pos_w = robot.data.body_pos_w[:, ee_body_idx] + ee_quat_w = robot.data.body_quat_w[:, ee_body_idx] + + # get jacobian to IK controller + jacobian = robot.root_physx_view.get_jacobians()[:, ee_body_idx, :, arm_joint_indices] + ik_controller.set_command(command=target_pos_tensor, ee_quat=ee_quat_w) + joint_pos_des = ik_controller.compute(ee_pos_w, ee_quat_w, jacobian, current_joint_pos) + + joint_pos_target = robot.data.joint_pos[0].clone() + + # Update joints: 6 from IK + 1 from button control (correct by design) + joint_pos_target[arm_joint_indices] = joint_pos_des[0] # panda_joint1-6 from IK + joint_pos_target[6] = ee_rotation_angle # panda_joint7 - end-effector rotation (button C) + joint_pos_target[[-2, -1]] = gripper_target # gripper + + robot.set_joint_position_target(joint_pos_target.unsqueeze(0)) + + for _ in range(5): + scene.write_data_to_sim() + sim.step() + + scene.update(sim_dt) + count += 1 + + # get contact forces and apply force feedback + left_finger_forces = left_finger_sensor.data.net_forces_w[0, 0] + right_finger_forces = right_finger_sensor.data.net_forces_w[0, 0] + total_contact_force = (left_finger_forces + right_finger_forces) * 0.5 + haply_device.push_force(forces=total_contact_force.unsqueeze(0), position=torch.tensor([0])) + + +def main(): + """Main function to set up and run the Haply teleoperation demo.""" + sim_cfg = sim_utils.SimulationCfg(device=args_cli.device, dt=1 / 200) + sim = sim_utils.SimulationContext(sim_cfg) + + # set the simulation view + sim.set_camera_view([1.6, 1.0, 1.70], [0.4, 0.0, 1.0]) + + scene_cfg = FrankaHaplySceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0) + scene = InteractiveScene(scene_cfg) + + # Create Haply device + haply_cfg = HaplyDeviceCfg( + websocket_uri=args_cli.websocket_uri, + pos_sensitivity=args_cli.pos_sensitivity, + sim_device=args_cli.device, + limit_force=2.0, + ) + haply_device = HaplyDevice(cfg=haply_cfg) + print(f"[INFO] Haply connected: {args_cli.websocket_uri}") + + sim.reset() + + run_simulator(sim, scene, haply_device) + + +if __name__ == "__main__": + main() + simulation_app.close() diff --git a/scripts/demos/markers.py b/scripts/demos/markers.py index b7497de64a16..6152dcf5226f 100644 --- a/scripts/demos/markers.py +++ b/scripts/demos/markers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/demos/multi_asset.py b/scripts/demos/multi_asset.py index 9ebbbb66370b..d104eb161d38 100644 --- a/scripts/demos/multi_asset.py +++ b/scripts/demos/multi_asset.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -37,7 +37,6 @@ import random -from isaacsim.core.utils.stage import get_current_stage from pxr import Gf, Sdf import isaaclab.sim as sim_utils @@ -52,6 +51,7 @@ ) from isaaclab.scene import InteractiveScene, InteractiveSceneCfg from isaaclab.sim import SimulationContext +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.utils import Timer, configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR diff --git a/scripts/demos/pick_and_place.py b/scripts/demos/pick_and_place.py index cc14dcb0a72c..c98998de1242 100644 --- a/scripts/demos/pick_and_place.py +++ b/scripts/demos/pick_and_place.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +11,7 @@ # add argparse arguments parser = argparse.ArgumentParser(description="Keyboard control for Isaac Lab Pick and Place.") +parser.add_argument("--num_envs", type=int, default=32, help="Number of environments to spawn.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments @@ -20,14 +21,15 @@ app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app -import torch +"""Rest everything follows.""" + from collections.abc import Sequence +import torch + import carb import omni -from isaaclab_assets.robots.pick_and_place import PICK_AND_PLACE_CFG - import isaaclab.sim as sim_utils from isaaclab.assets import ( Articulation, @@ -45,6 +47,8 @@ from isaaclab.utils import configclass from isaaclab.utils.math import sample_uniform +from isaaclab_assets.robots.pick_and_place import PICK_AND_PLACE_CFG + @configclass class PickAndPlaceEnvCfg(DirectRLEnvCfg): @@ -59,11 +63,16 @@ class PickAndPlaceEnvCfg(DirectRLEnvCfg): action_space = 4 observation_space = 6 state_space = 0 - device = "cpu" - # Simulation cfg. Note that we are forcing the simulation to run on CPU. - # This is because the surface gripper API is only supported on CPU backend for now. - sim: SimulationCfg = SimulationCfg(dt=1 / 60, render_interval=decimation, device="cpu") + # Simulation cfg. Surface grippers are currently only supported on CPU. + # Surface grippers also require scene query support to function. + sim: SimulationCfg = SimulationCfg( + dt=1 / 60, + device="cpu", + render_interval=decimation, + use_fabric=True, + enable_scene_query_support=True, + ) debug_vis = True # robot @@ -136,8 +145,8 @@ def __init__(self, cfg: PickAndPlaceEnvCfg, render_mode: str | None = None, **kw self.joint_vel = self.pick_and_place.data.joint_vel # Buffers - self.go_to_cube = False - self.go_to_target = False + self.go_to_cube = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self.go_to_target = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) self.target_pos = torch.zeros((self.num_envs, 3), device=self.device, dtype=torch.float32) self.instant_controls = torch.zeros((self.num_envs, 3), device=self.device, dtype=torch.float32) self.permanent_controls = torch.zeros((self.num_envs, 1), device=self.device, dtype=torch.float32) @@ -173,35 +182,36 @@ def set_up_keyboard(self): print("Keyboard set up!") print("The simulation is ready for you to try it out!") print("Your goal is pick up the purple cube and to drop it on the red sphere!") - print("Use the following controls to interact with the simulation:") - print("Press the 'A' key to have the gripper track the cube position.") - print("Press the 'D' key to have the gripper track the target position") - print("Press the 'W' or 'S' keys to move the gantry UP or DOWN respectively") - print("Press 'Q' or 'E' to OPEN or CLOSE the gripper respectively") + print(f"Number of environments: {self.num_envs}") + print("Use the following controls to interact with ALL environments simultaneously:") + print("Press the 'A' key to have all grippers track the cube position.") + print("Press the 'D' key to have all grippers track the target position") + print("Press the 'W' or 'S' keys to move all gantries UP or DOWN respectively") + print("Press 'Q' or 'E' to OPEN or CLOSE all grippers respectively") def _on_keyboard_event(self, event): """Checks for a keyboard event and assign the corresponding command control depending on key pressed.""" if event.type == carb.input.KeyboardEventType.KEY_PRESS: - # Logic on key press + # Logic on key press - apply to ALL environments if event.input.name == self._auto_aim_target: - self.go_to_target = True - self.go_to_cube = False + self.go_to_target[:] = True + self.go_to_cube[:] = False if event.input.name == self._auto_aim_cube: - self.go_to_cube = True - self.go_to_target = False + self.go_to_cube[:] = True + self.go_to_target[:] = False if event.input.name in self._instant_key_controls: - self.go_to_cube = False - self.go_to_target = False - self.instant_controls[0] = self._instant_key_controls[event.input.name] + self.go_to_cube[:] = False + self.go_to_target[:] = False + self.instant_controls[:] = self._instant_key_controls[event.input.name] if event.input.name in self._permanent_key_controls: - self.go_to_cube = False - self.go_to_target = False - self.permanent_controls[0] = self._permanent_key_controls[event.input.name] - # On key release, the robot stops moving + self.go_to_cube[:] = False + self.go_to_target[:] = False + self.permanent_controls[:] = self._permanent_key_controls[event.input.name] + # On key release, all robots stop moving elif event.type == carb.input.KeyboardEventType.KEY_RELEASE: - self.go_to_cube = False - self.go_to_target = False - self.instant_controls[0] = self._instant_key_controls["ZEROS"] + self.go_to_cube[:] = False + self.go_to_target[:] = False + self.instant_controls[:] = self._instant_key_controls["ZEROS"] def _setup_scene(self): self.pick_and_place = Articulation(self.cfg.robot_cfg) @@ -225,28 +235,30 @@ def _pre_physics_step(self, actions: torch.Tensor) -> None: def _apply_action(self) -> None: # We use the keyboard outputs as an action. - if self.go_to_cube: + # Process each environment independently + if self.go_to_cube.any(): # Effort based proportional controller to track the cube position - head_pos_x = self.pick_and_place.data.joint_pos[:, self._x_dof_idx[0]] - head_pos_y = self.pick_and_place.data.joint_pos[:, self._y_dof_idx[0]] - cube_pos_x = self.cube.data.root_pos_w[:, 0] - self.scene.env_origins[:, 0] - cube_pos_y = self.cube.data.root_pos_w[:, 1] - self.scene.env_origins[:, 1] + head_pos_x = self.pick_and_place.data.joint_pos[self.go_to_cube, self._x_dof_idx[0]] + head_pos_y = self.pick_and_place.data.joint_pos[self.go_to_cube, self._y_dof_idx[0]] + cube_pos_x = self.cube.data.root_pos_w[self.go_to_cube, 0] - self.scene.env_origins[self.go_to_cube, 0] + cube_pos_y = self.cube.data.root_pos_w[self.go_to_cube, 1] - self.scene.env_origins[self.go_to_cube, 1] d_cube_robot_x = cube_pos_x - head_pos_x d_cube_robot_y = cube_pos_y - head_pos_y - self.instant_controls[0] = torch.tensor( - [d_cube_robot_x * 5.0, d_cube_robot_y * 5.0, 0.0], device=self.device + self.instant_controls[self.go_to_cube] = torch.stack( + [d_cube_robot_x * 5.0, d_cube_robot_y * 5.0, torch.zeros_like(d_cube_robot_x)], dim=1 ) - elif self.go_to_target: + if self.go_to_target.any(): # Effort based proportional controller to track the target position - head_pos_x = self.pick_and_place.data.joint_pos[:, self._x_dof_idx[0]] - head_pos_y = self.pick_and_place.data.joint_pos[:, self._y_dof_idx[0]] - target_pos_x = self.target_pos[:, 0] - target_pos_y = self.target_pos[:, 1] + head_pos_x = self.pick_and_place.data.joint_pos[self.go_to_target, self._x_dof_idx[0]] + head_pos_y = self.pick_and_place.data.joint_pos[self.go_to_target, self._y_dof_idx[0]] + target_pos_x = self.target_pos[self.go_to_target, 0] + target_pos_y = self.target_pos[self.go_to_target, 1] d_target_robot_x = target_pos_x - head_pos_x d_target_robot_y = target_pos_y - head_pos_y - self.instant_controls[0] = torch.tensor( - [d_target_robot_x * 5.0, d_target_robot_y * 5.0, 0.0], device=self.device + self.instant_controls[self.go_to_target] = torch.stack( + [d_target_robot_x * 5.0, d_target_robot_y * 5.0, torch.zeros_like(d_target_robot_x)], dim=1 ) + # Set the joint effort targets for the picker self.pick_and_place.set_joint_effort_target( self.instant_controls[:, 0].unsqueeze(dim=1), joint_ids=self._x_dof_idx @@ -258,7 +270,7 @@ def _apply_action(self) -> None: self.permanent_controls[:, 0].unsqueeze(dim=1), joint_ids=self._z_dof_idx ) # Set the gripper command - self.gripper.set_grippers_command(self.instant_controls[:, 2].unsqueeze(dim=1)) + self.gripper.set_grippers_command(self.instant_controls[:, 2]) def _get_observations(self) -> dict: # Get the observations @@ -397,8 +409,11 @@ def _debug_vis_callback(self, event): def main(): """Main function.""" + # create environment configuration + env_cfg = PickAndPlaceEnvCfg() + env_cfg.scene.num_envs = args_cli.num_envs # create environment - pick_and_place = PickAndPlaceEnv(PickAndPlaceEnvCfg()) + pick_and_place = PickAndPlaceEnv(env_cfg) obs, _ = pick_and_place.reset() while simulation_app.is_running(): # check for selected robots diff --git a/scripts/demos/procedural_terrain.py b/scripts/demos/procedural_terrain.py index 36b27be253ed..f0a2fb4e2ef7 100644 --- a/scripts/demos/procedural_terrain.py +++ b/scripts/demos/procedural_terrain.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -66,6 +66,7 @@ """Rest everything follows.""" import random + import torch import isaaclab.sim as sim_utils diff --git a/scripts/demos/quadcopter.py b/scripts/demos/quadcopter.py index 7618a387b770..bf42a04f8501 100644 --- a/scripts/demos/quadcopter.py +++ b/scripts/demos/quadcopter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -101,7 +101,11 @@ def main(): forces = torch.zeros(robot.num_instances, 4, 3, device=sim.device) torques = torch.zeros_like(forces) forces[..., 2] = robot_mass * gravity / 4.0 - robot.set_external_force_and_torque(forces, torques, body_ids=prop_body_ids) + robot.permanent_wrench_composer.set_forces_and_torques( + forces=forces, + torques=torques, + body_ids=prop_body_ids, + ) robot.write_data_to_sim() # perform step sim.step() diff --git a/scripts/demos/quadrupeds.py b/scripts/demos/quadrupeds.py index f22dcb1f26f5..b9935de30dab 100644 --- a/scripts/demos/quadrupeds.py +++ b/scripts/demos/quadrupeds.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,8 +35,6 @@ import numpy as np import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.assets import Articulation @@ -77,37 +75,37 @@ def design_scene() -> tuple[dict, list[list[float]]]: origins = define_origins(num_origins=7, spacing=1.25) # Origin 1 with Anymal B - prim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) + sim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) # -- Robot anymal_b = Articulation(ANYMAL_B_CFG.replace(prim_path="/World/Origin1/Robot")) # Origin 2 with Anymal C - prim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) + sim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) # -- Robot anymal_c = Articulation(ANYMAL_C_CFG.replace(prim_path="/World/Origin2/Robot")) # Origin 3 with Anymal D - prim_utils.create_prim("/World/Origin3", "Xform", translation=origins[2]) + sim_utils.create_prim("/World/Origin3", "Xform", translation=origins[2]) # -- Robot anymal_d = Articulation(ANYMAL_D_CFG.replace(prim_path="/World/Origin3/Robot")) # Origin 4 with Unitree A1 - prim_utils.create_prim("/World/Origin4", "Xform", translation=origins[3]) + sim_utils.create_prim("/World/Origin4", "Xform", translation=origins[3]) # -- Robot unitree_a1 = Articulation(UNITREE_A1_CFG.replace(prim_path="/World/Origin4/Robot")) # Origin 5 with Unitree Go1 - prim_utils.create_prim("/World/Origin5", "Xform", translation=origins[4]) + sim_utils.create_prim("/World/Origin5", "Xform", translation=origins[4]) # -- Robot unitree_go1 = Articulation(UNITREE_GO1_CFG.replace(prim_path="/World/Origin5/Robot")) # Origin 6 with Unitree Go2 - prim_utils.create_prim("/World/Origin6", "Xform", translation=origins[5]) + sim_utils.create_prim("/World/Origin6", "Xform", translation=origins[5]) # -- Robot unitree_go2 = Articulation(UNITREE_GO2_CFG.replace(prim_path="/World/Origin6/Robot")) # Origin 7 with Boston Dynamics Spot - prim_utils.create_prim("/World/Origin7", "Xform", translation=origins[6]) + sim_utils.create_prim("/World/Origin7", "Xform", translation=origins[6]) # -- Robot spot = Articulation(SPOT_CFG.replace(prim_path="/World/Origin7/Robot")) diff --git a/scripts/demos/sensors/cameras.py b/scripts/demos/sensors/cameras.py index ad2bc8c150d0..83214f7e4cf2 100644 --- a/scripts/demos/sensors/cameras.py +++ b/scripts/demos/sensors/cameras.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -37,9 +37,10 @@ """Rest everything follows.""" +import os + import matplotlib.pyplot as plt import numpy as np -import os import torch import isaaclab.sim as sim_utils @@ -138,7 +139,10 @@ def save_images_grid( ncol = int(np.ceil(n_images / nrow)) fig, axes = plt.subplots(nrow, ncol, figsize=(ncol * 2, nrow * 2)) - axes = axes.flatten() + if isinstance(axes, np.ndarray): + axes = axes.flatten() + else: + axes = np.array([axes]) # plot images for idx, (img, ax) in enumerate(zip(images, axes)): diff --git a/scripts/demos/sensors/contact_sensor.py b/scripts/demos/sensors/contact_sensor.py index 0296b4fa728c..0ee672ec16a6 100644 --- a/scripts/demos/sensors/contact_sensor.py +++ b/scripts/demos/sensors/contact_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -99,7 +99,6 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # Simulate physics while simulation_app.is_running(): - if count % 500 == 0: # reset counter count = 0 diff --git a/scripts/demos/sensors/frame_transformer_sensor.py b/scripts/demos/sensors/frame_transformer_sensor.py index ef379c2a9ad5..8827b23cea71 100644 --- a/scripts/demos/sensors/frame_transformer_sensor.py +++ b/scripts/demos/sensors/frame_transformer_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -95,7 +95,6 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # Simulate physics while simulation_app.is_running(): - if count % 500 == 0: # reset counter count = 0 diff --git a/scripts/demos/sensors/imu_sensor.py b/scripts/demos/sensors/imu_sensor.py index 22d6e74758e6..af649fd94a97 100644 --- a/scripts/demos/sensors/imu_sensor.py +++ b/scripts/demos/sensors/imu_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -66,7 +66,6 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # Simulate physics while simulation_app.is_running(): - if count % 500 == 0: # reset counter count = 0 diff --git a/scripts/demos/sensors/multi_mesh_raycaster.py b/scripts/demos/sensors/multi_mesh_raycaster.py new file mode 100644 index 000000000000..07b36573501b --- /dev/null +++ b/scripts/demos/sensors/multi_mesh_raycaster.py @@ -0,0 +1,303 @@ +# 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 + + +"""Example on using the Multi-Mesh Raycaster sensor. + +.. code-block:: bash + + # with allegro hand + python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type allegro_hand + + # with anymal-D bodies + python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type anymal_d + + # with random multiple objects + python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type objects + +""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Example on using the multi-mesh raycaster sensor.") +parser.add_argument("--num_envs", type=int, default=16, help="Number of environments to spawn.") +parser.add_argument( + "--asset_type", + type=str, + default="allegro_hand", + help="Asset type to use.", + choices=["allegro_hand", "anymal_d", "objects"], +) +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import random + +import torch + +import omni.usd +from pxr import Gf, Sdf + +import isaaclab.sim as sim_utils +from isaaclab.assets import Articulation, AssetBaseCfg, RigidObjectCfg +from isaaclab.markers.config import VisualizationMarkersCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.sensors.ray_caster import MultiMeshRayCasterCfg, patterns +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +## +# Pre-defined configs +## +from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG +from isaaclab_assets.robots.anymal import ANYMAL_D_CFG + +RAY_CASTER_MARKER_CFG = VisualizationMarkersCfg( + markers={ + "hit": sim_utils.SphereCfg( + radius=0.01, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)), + ), + }, +) + + +if args_cli.asset_type == "allegro_hand": + asset_cfg = ALLEGRO_HAND_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + ray_caster_cfg = MultiMeshRayCasterCfg( + prim_path="{ENV_REGEX_NS}/Robot", + update_period=1 / 60, + offset=MultiMeshRayCasterCfg.OffsetCfg(pos=(0, -0.1, 0.3)), + mesh_prim_paths=[ + "/World/Ground", + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/thumb_link_.*/visuals_xform"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/index_link.*/visuals_xform"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/middle_link_.*/visuals_xform"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/ring_link_.*/visuals_xform"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/palm_link/visuals_xform"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/allegro_mount/visuals_xform"), + ], + ray_alignment="world", + pattern_cfg=patterns.GridPatternCfg(resolution=0.005, size=(0.4, 0.4), direction=(0, 0, -1)), + debug_vis=not args_cli.headless, + visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"), + ) + +elif args_cli.asset_type == "anymal_d": + asset_cfg = ANYMAL_D_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + ray_caster_cfg = MultiMeshRayCasterCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + update_period=1 / 60, + offset=MultiMeshRayCasterCfg.OffsetCfg(pos=(0, -0.1, 0.3)), + mesh_prim_paths=[ + "/World/Ground", + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LF_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RF_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LH_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RH_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/base/visuals"), + ], + ray_alignment="world", + pattern_cfg=patterns.GridPatternCfg(resolution=0.02, size=(2.5, 2.5), direction=(0, 0, -1)), + debug_vis=not args_cli.headless, + visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"), + ) + +elif args_cli.asset_type == "objects": + asset_cfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Object", + spawn=sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + ), + sim_utils.CylinderCfg( + radius=0.2, + height=0.5, + axis="Y", + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + sim_utils.CapsuleCfg( + radius=0.15, + height=0.5, + axis="Z", + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 1.0, 0.0), metallic=0.2), + ), + sim_utils.ConeCfg( + radius=0.2, + height=0.5, + axis="Z", + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 1.0), metallic=0.2), + ), + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)), + ) + ray_caster_cfg = MultiMeshRayCasterCfg( + prim_path="{ENV_REGEX_NS}/Object", + update_period=1 / 60, + offset=MultiMeshRayCasterCfg.OffsetCfg(pos=(0, 0.0, 0.6)), + mesh_prim_paths=[ + "/World/Ground", + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Object"), + ], + ray_alignment="world", + pattern_cfg=patterns.GridPatternCfg(resolution=0.01, size=(0.6, 0.6), direction=(0, 0, -1)), + debug_vis=not args_cli.headless, + visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"), + ) +else: + raise ValueError(f"Unknown asset type: {args_cli.asset_type}") + + +@configclass +class RaycasterSensorSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the asset.""" + + # ground plane + ground = AssetBaseCfg( + prim_path="/World/Ground", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd", + scale=(1, 1, 1), + ), + ) + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # asset + asset = asset_cfg + # ray caster + ray_caster = ray_caster_cfg + + +def randomize_shape_color(prim_path_expr: str): + """Randomize the color of the geometry.""" + + # acquire stage + stage = omni.usd.get_context().get_stage() + # resolve prim paths for spawning and cloning + prim_paths = sim_utils.find_matching_prim_paths(prim_path_expr) + # manually clone prims if the source prim path is a regex expression + + with Sdf.ChangeBlock(): + for prim_path in prim_paths: + print("Applying prim scale to:", prim_path) + # spawn single instance + prim_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) + + # DO YOUR OWN OTHER KIND OF RANDOMIZATION HERE! + # Note: Just need to acquire the right attribute about the property you want to set + # Here is an example on setting color randomly + color_spec = prim_spec.GetAttributeAtPath(prim_path + "/geometry/material/Shader.inputs:diffuseColor") + color_spec.default = Gf.Vec3f(random.random(), random.random(), random.random()) + + # randomize scale + scale_spec = prim_spec.GetAttributeAtPath(prim_path + ".xformOp:scale") + scale_spec.default = Gf.Vec3f(random.uniform(0.5, 1.5), random.uniform(0.5, 1.5), random.uniform(0.5, 1.5)) + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Run the simulator.""" + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + + # Simulate physics + while simulation_app.is_running(): + if count % 500 == 0: + # reset counter + count = 0 + # reset the scene entities + # root state + root_state = scene["asset"].data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + scene["asset"].write_root_pose_to_sim(root_state[:, :7]) + scene["asset"].write_root_velocity_to_sim(root_state[:, 7:]) + + if isinstance(scene["asset"], Articulation): + # set joint positions with some noise + joint_pos, joint_vel = ( + scene["asset"].data.default_joint_pos.clone(), + scene["asset"].data.default_joint_vel.clone(), + ) + joint_pos += torch.rand_like(joint_pos) * 0.1 + scene["asset"].write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + print("[INFO]: Resetting Asset state...") + + if isinstance(scene["asset"], Articulation): + # -- generate actions/commands + targets = scene["asset"].data.default_joint_pos + 5 * ( + torch.rand_like(scene["asset"].data.default_joint_pos) - 0.5 + ) + # -- apply action to the asset + scene["asset"].set_joint_position_target(targets) + # -- write data to sim + scene.write_data_to_sim() + # perform step + sim.step() + # update sim-time + sim_time += sim_dt + count += 1 + # update buffers + scene.update(sim_dt) + + +def main(): + """Main function.""" + + # Initialize the simulation context + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = sim_utils.SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0]) + # design scene + scene_cfg = RaycasterSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=False) + scene = InteractiveScene(scene_cfg) + + if args_cli.asset_type == "objects": + randomize_shape_color(scene_cfg.asset.prim_path.format(ENV_REGEX_NS="/World/envs/env_.*")) + + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim, scene) + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/demos/sensors/multi_mesh_raycaster_camera.py b/scripts/demos/sensors/multi_mesh_raycaster_camera.py new file mode 100644 index 000000000000..8ef3a188f3d1 --- /dev/null +++ b/scripts/demos/sensors/multi_mesh_raycaster_camera.py @@ -0,0 +1,329 @@ +# 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 + + +"""Example on using the Multi-Mesh Raycaster Camera sensor. + +.. code-block:: bash + + # with allegro hand + python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type allegro_hand + + # with anymal-D bodies + python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type anymal_d + + # with random multiple objects + python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type objects + +""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Example on using the multi-mesh raycaster sensor.") +parser.add_argument("--num_envs", type=int, default=16, help="Number of environments to spawn.") +parser.add_argument( + "--asset_type", + type=str, + default="allegro_hand", + help="Asset type to use.", + choices=["allegro_hand", "anymal_d", "objects"], +) +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import random + +import torch + +import omni.usd +from pxr import Gf, Sdf + +import isaaclab.sim as sim_utils +from isaaclab.assets import Articulation, AssetBaseCfg, RigidObjectCfg +from isaaclab.markers.config import VisualizationMarkersCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.sensors.ray_caster import MultiMeshRayCasterCameraCfg, patterns +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +## +# Pre-defined configs +## +from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG +from isaaclab_assets.robots.anymal import ANYMAL_D_CFG + +RAY_CASTER_MARKER_CFG = VisualizationMarkersCfg( + markers={ + "hit": sim_utils.SphereCfg( + radius=0.01, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)), + ), + }, +) + +if args_cli.asset_type == "allegro_hand": + asset_cfg = ALLEGRO_HAND_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + ray_caster_cfg = MultiMeshRayCasterCameraCfg( + prim_path="{ENV_REGEX_NS}/Robot", + update_period=1 / 60, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg( + pos=(-0.70, -0.7, -0.25), rot=(0.268976, 0.268976, 0.653951, 0.653951) + ), + mesh_prim_paths=[ + "/World/Ground", + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/thumb_link_.*/visuals_xform"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/index_link.*/visuals_xform"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/middle_link_.*/visuals_xform"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/ring_link_.*/visuals_xform"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/palm_link/visuals_xform"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/allegro_mount/visuals_xform"), + ], + pattern_cfg=patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=120, + width=240, + ), + debug_vis=not args_cli.headless, + visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"), + ) + +elif args_cli.asset_type == "anymal_d": + asset_cfg = ANYMAL_D_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + ray_caster_cfg = MultiMeshRayCasterCameraCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + update_period=1 / 60, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(0, -0.1, 1.5), rot=(0.0, 1.0, 0.0, 0.0)), + mesh_prim_paths=[ + "/World/Ground", + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LF_.*/visuals"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RF_.*/visuals"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LH_.*/visuals"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RH_.*/visuals"), + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/base/visuals"), + ], + pattern_cfg=patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=120, + width=240, + ), + debug_vis=not args_cli.headless, + visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"), + ) + +elif args_cli.asset_type == "objects": + asset_cfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Object", + spawn=sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + ), + sim_utils.CylinderCfg( + radius=0.2, + height=0.5, + axis="Y", + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + sim_utils.CapsuleCfg( + radius=0.15, + height=0.5, + axis="Z", + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 1.0, 0.0), metallic=0.2), + ), + sim_utils.ConeCfg( + radius=0.2, + height=0.5, + axis="Z", + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 1.0), metallic=0.2), + ), + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)), + ) + ray_caster_cfg = MultiMeshRayCasterCameraCfg( + prim_path="{ENV_REGEX_NS}/Object", + update_period=1 / 60, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(0, 0.0, 1.5), rot=(0.0, 1.0, 0.0, 0.0)), + mesh_prim_paths=[ + "/World/Ground", + MultiMeshRayCasterCameraCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Object"), + ], + pattern_cfg=patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=120, + width=240, + ), + debug_vis=not args_cli.headless, + visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"), + ) +else: + raise ValueError(f"Unknown asset type: {args_cli.asset_type}") + + +@configclass +class RaycasterSensorSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the asset.""" + + # ground plane + ground = AssetBaseCfg( + prim_path="/World/Ground", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd", + scale=(1, 1, 1), + ), + ) + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # asset + asset = asset_cfg + # ray caster + ray_caster = ray_caster_cfg + + +def randomize_shape_color(prim_path_expr: str): + """Randomize the color of the geometry.""" + + # acquire stage + stage = omni.usd.get_context().get_stage() + # resolve prim paths for spawning and cloning + prim_paths = sim_utils.find_matching_prim_paths(prim_path_expr) + # manually clone prims if the source prim path is a regex expression + + with Sdf.ChangeBlock(): + for prim_path in prim_paths: + print("Applying prim scale to:", prim_path) + # spawn single instance + prim_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) + + # DO YOUR OWN OTHER KIND OF RANDOMIZATION HERE! + # Note: Just need to acquire the right attribute about the property you want to set + # Here is an example on setting color randomly + color_spec = prim_spec.GetAttributeAtPath(prim_path + "/geometry/material/Shader.inputs:diffuseColor") + color_spec.default = Gf.Vec3f(random.random(), random.random(), random.random()) + + # randomize scale + scale_spec = prim_spec.GetAttributeAtPath(prim_path + ".xformOp:scale") + scale_spec.default = Gf.Vec3f(random.uniform(0.5, 1.5), random.uniform(0.5, 1.5), random.uniform(0.5, 1.5)) + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Run the simulator.""" + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + + triggered = True + countdown = 42 + + # Simulate physics + while simulation_app.is_running(): + if count % 500 == 0: + # reset counter + count = 0 + # reset the scene entities + # root state + root_state = scene["asset"].data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + scene["asset"].write_root_pose_to_sim(root_state[:, :7]) + scene["asset"].write_root_velocity_to_sim(root_state[:, 7:]) + + if isinstance(scene["asset"], Articulation): + # set joint positions with some noise + joint_pos, joint_vel = ( + scene["asset"].data.default_joint_pos.clone(), + scene["asset"].data.default_joint_vel.clone(), + ) + joint_pos += torch.rand_like(joint_pos) * 0.1 + scene["asset"].write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + print("[INFO]: Resetting Asset state...") + + if isinstance(scene["asset"], Articulation): + # -- generate actions/commands + targets = scene["asset"].data.default_joint_pos + 5 * ( + torch.rand_like(scene["asset"].data.default_joint_pos) - 0.5 + ) + # -- apply action to the asset + scene["asset"].set_joint_position_target(targets) + # -- write data to sim + scene.write_data_to_sim() + # perform step + sim.step() + # update sim-time + sim_time += sim_dt + count += 1 + # update buffers + scene.update(sim_dt) + + if not triggered: + if countdown > 0: + countdown -= 1 + continue + + data = scene["ray_caster"].data.ray_hits_w.cpu().numpy() # noqa: F841 + triggered = True + else: + continue + + +def main(): + """Main function.""" + + # Initialize the simulation context + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = sim_utils.SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0]) + # design scene + scene_cfg = RaycasterSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=False) + scene = InteractiveScene(scene_cfg) + + if args_cli.asset_type == "objects": + randomize_shape_color(scene_cfg.asset.prim_path.format(ENV_REGEX_NS="/World/envs/env_.*")) + + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim, scene) + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/demos/sensors/raycaster_sensor.py b/scripts/demos/sensors/raycaster_sensor.py index 71eac60e07b3..02c55222e836 100644 --- a/scripts/demos/sensors/raycaster_sensor.py +++ b/scripts/demos/sensors/raycaster_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -83,7 +83,6 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): # Simulate physics while simulation_app.is_running(): - if count % 500 == 0: # reset counter count = 0 diff --git a/scripts/demos/sensors/tacsl_sensor.py b/scripts/demos/sensors/tacsl_sensor.py new file mode 100644 index 000000000000..aa5f2253e293 --- /dev/null +++ b/scripts/demos/sensors/tacsl_sensor.py @@ -0,0 +1,427 @@ +# 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 + +""" +Example script demonstrating the TacSL tactile sensor implementation in IsaacLab. + +This script shows how to use the TactileSensor for both camera-based and force field +tactile sensing with the GelSight finger setup. + +.. code-block:: bash + + # Usage + python scripts/demos/sensors/tacsl_sensor.py \ + --use_tactile_rgb \ + --use_tactile_ff \ + --tactile_compliance_stiffness 100.0 \ + --num_envs 16 \ + --contact_object_type nut \ + --save_viz \ + --enable_cameras + +""" + +import argparse +import math +import os + +import cv2 +import numpy as np +import torch + +from isaaclab.app import AppLauncher + +# Add argparse arguments +parser = argparse.ArgumentParser(description="TacSL tactile sensor example.") +parser.add_argument("--num_envs", type=int, default=2, help="Number of environments to spawn.") +parser.add_argument("--normal_contact_stiffness", type=float, default=1.0, help="Tactile normal stiffness.") +parser.add_argument("--tangential_stiffness", type=float, default=0.1, help="Tactile tangential stiffness.") +parser.add_argument("--friction_coefficient", type=float, default=2.0, help="Tactile friction coefficient.") +parser.add_argument( + "--tactile_compliance_stiffness", + type=float, + default=None, + help="Optional: Override compliant contact stiffness (default: use USD asset values)", +) +parser.add_argument( + "--tactile_compliant_damping", + type=float, + default=None, + help="Optional: Override compliant contact damping (default: use USD asset values)", +) +parser.add_argument("--save_viz", action="store_true", help="Visualize tactile data.") +parser.add_argument("--save_viz_dir", type=str, default="tactile_record", help="Directory to save tactile data.") +parser.add_argument("--use_tactile_rgb", action="store_true", help="Use tactile RGB sensor data collection.") +parser.add_argument("--use_tactile_ff", action="store_true", help="Use tactile force field sensor data collection.") +parser.add_argument("--debug_sdf_closest_pts", action="store_true", help="Visualize closest SDF points.") +parser.add_argument("--debug_tactile_sensor_pts", action="store_true", help="Visualize tactile sensor points.") +parser.add_argument("--trimesh_vis_tactile_points", action="store_true", help="Visualize tactile points using trimesh.") +parser.add_argument( + "--contact_object_type", + type=str, + default="nut", + choices=["none", "cube", "nut"], + help="Type of contact object to use.", +) + +# 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.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.sensors import TiledCameraCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +from isaaclab.utils.timer import Timer + +from isaaclab_contrib.sensors.tacsl_sensor import VisuoTactileSensorCfg +from isaaclab_contrib.sensors.tacsl_sensor.visuotactile_render import compute_tactile_shear_image +from isaaclab_contrib.sensors.tacsl_sensor.visuotactile_sensor_data import VisuoTactileSensorData + +from isaaclab_assets.sensors import GELSIGHT_R15_CFG + + +@configclass +class TactileSensorsSceneCfg(InteractiveSceneCfg): + """Design the scene with tactile sensors on the robot.""" + + # Ground plane + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + + # Lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # Robot with tactile sensor + robot = ArticulationCfg( + prim_path="{ENV_REGEX_NS}/Robot", + spawn=sim_utils.UsdFileWithCompliantContactCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/TacSL/gelsight_r15_finger/gelsight_r15_finger.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + ), + compliant_contact_stiffness=args_cli.tactile_compliance_stiffness, + compliant_contact_damping=args_cli.tactile_compliant_damping, + physics_material_prim_path="elastomer", + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=12, + solver_velocity_iteration_count=1, + ), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.001, rest_offset=-0.0005), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.5), + rot=(math.sqrt(2) / 2, -math.sqrt(2) / 2, 0.0, 0.0), # 90° rotation + joint_pos={}, + joint_vel={}, + ), + actuators={}, + ) + + # Camera configuration for tactile sensing + + # TacSL Tactile Sensor + tactile_sensor = VisuoTactileSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/elastomer/tactile_sensor", + history_length=0, + debug_vis=args_cli.debug_tactile_sensor_pts or args_cli.debug_sdf_closest_pts, + # Sensor configuration + render_cfg=GELSIGHT_R15_CFG, + enable_camera_tactile=args_cli.use_tactile_rgb, + enable_force_field=args_cli.use_tactile_ff, + # Elastomer configuration + tactile_array_size=(20, 25), + tactile_margin=0.003, + # Contact object configuration + contact_object_prim_path_expr="{ENV_REGEX_NS}/contact_object", + # Force field physics parameters + normal_contact_stiffness=args_cli.normal_contact_stiffness, + friction_coefficient=args_cli.friction_coefficient, + tangential_stiffness=args_cli.tangential_stiffness, + # Camera configuration + # Note: the camera is already spawned in the scene, properties are set in the + # 'gelsight_r15_finger.usd' USD file + camera_cfg=TiledCameraCfg( + prim_path="{ENV_REGEX_NS}/Robot/elastomer_tip/cam", + height=GELSIGHT_R15_CFG.image_height, + width=GELSIGHT_R15_CFG.image_width, + data_types=["distance_to_image_plane"], + spawn=None, + ), + # Debug Visualization + trimesh_vis_tactile_points=args_cli.trimesh_vis_tactile_points, + visualize_sdf_closest_pts=args_cli.debug_sdf_closest_pts, + ) + + +@configclass +class CubeTactileSceneCfg(TactileSensorsSceneCfg): + """Scene with cube contact object.""" + + # Cube contact object + contact_object = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/contact_object", + spawn=sim_utils.CuboidCfg( + size=(0.01, 0.01, 0.01), + rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=True), + mass_props=sim_utils.MassPropertiesCfg(mass=0.00327211), + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=sim_utils.RigidBodyMaterialCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.1, 0.1)), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0 + 0.06776, 0.51), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +@configclass +class NutTactileSceneCfg(TactileSensorsSceneCfg): + """Scene with nut contact object.""" + + # Nut contact object + contact_object = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/contact_object", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Factory/factory_nut_m16.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + solver_position_iteration_count=12, + solver_velocity_iteration_count=1, + max_angular_velocity=180.0, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.1), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0), + articulation_props=sim_utils.ArticulationRootPropertiesCfg(articulation_enabled=False), + ), + init_state=RigidObjectCfg.InitialStateCfg( + pos=(0.0, 0.0 + 0.06776, 0.498), + rot=(1.0, 0.0, 0.0, 0.0), + ), + ) + + +def mkdir_helper(dir_path: str) -> tuple[str, str]: + """Create directories for saving tactile sensor visualizations. + + Args: + dir_path: The base directory path where visualizations will be saved. + + Returns: + A tuple containing paths to the force field directory and RGB image directory. + """ + tactile_img_folder = dir_path + os.makedirs(tactile_img_folder, exist_ok=True) + # create a subdirectory for the force field data + tactile_force_field_dir = os.path.join(tactile_img_folder, "tactile_force_field") + os.makedirs(tactile_force_field_dir, exist_ok=True) + # create a subdirectory for the RGB image data + tactile_rgb_image_dir = os.path.join(tactile_img_folder, "tactile_rgb_image") + os.makedirs(tactile_rgb_image_dir, exist_ok=True) + + return tactile_force_field_dir, tactile_rgb_image_dir + + +def save_viz_helper( + dir_path_list: tuple[str, str], + count: int, + tactile_data: VisuoTactileSensorData, + num_envs: int, + nrows: int, + ncols: int, +): + """Save visualization of tactile sensor data. + + Args: + dir_path_list: A tuple containing paths to the force field directory and RGB image directory. + count: The current simulation step count, used for naming saved files. + tactile_data: The data object containing tactile sensor readings (forces, images). + num_envs: Number of environments in the simulation. + nrows: Number of rows in the tactile array. + ncols: Number of columns in the tactile array. + """ + # Only save the first 2 environments + + tactile_force_field_dir, tactile_rgb_image_dir = dir_path_list + + if tactile_data.tactile_shear_force is not None and tactile_data.tactile_normal_force is not None: + # visualize tactile forces + tactile_normal_force = tactile_data.tactile_normal_force.view((num_envs, nrows, ncols)) + tactile_shear_force = tactile_data.tactile_shear_force.view((num_envs, nrows, ncols, 2)) + + tactile_image = compute_tactile_shear_image( + tactile_normal_force[0, :, :].detach().cpu().numpy(), tactile_shear_force[0, :, :].detach().cpu().numpy() + ) + + if tactile_normal_force.shape[0] > 1: + tactile_image_1 = compute_tactile_shear_image( + tactile_normal_force[1, :, :].detach().cpu().numpy(), + tactile_shear_force[1, :, :].detach().cpu().numpy(), + ) + combined_image = np.vstack([tactile_image, tactile_image_1]) + cv2.imwrite( + os.path.join(tactile_force_field_dir, f"{count:04d}.png"), (combined_image * 255).astype(np.uint8) + ) + else: + cv2.imwrite( + os.path.join(tactile_force_field_dir, f"{count:04d}.png"), (tactile_image * 255).astype(np.uint8) + ) + + if tactile_data.tactile_rgb_image is not None: + tactile_rgb_data = tactile_data.tactile_rgb_image.cpu().numpy() + tactile_rgb_data = np.transpose(tactile_rgb_data, axes=(0, 2, 1, 3)) + tactile_rgb_data_first_2 = tactile_rgb_data[:2] if len(tactile_rgb_data) >= 2 else tactile_rgb_data + tactile_rgb_tiled = np.concatenate(tactile_rgb_data_first_2, axis=0) + # Convert to uint8 if not already + if tactile_rgb_tiled.dtype != np.uint8: + tactile_rgb_tiled = ( + (tactile_rgb_tiled * 255).astype(np.uint8) + if tactile_rgb_tiled.max() <= 1.0 + else tactile_rgb_tiled.astype(np.uint8) + ) + cv2.imwrite(os.path.join(tactile_rgb_image_dir, f"{count:04d}.png"), tactile_rgb_tiled) + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Run the simulator.""" + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + + # Assign different masses to contact objects in different environments + num_envs = scene.num_envs + + if args_cli.save_viz: + # Create output directories for tactile data + print(f"[INFO]: Saving tactile data to: {args_cli.save_viz_dir}...") + dir_path_list = mkdir_helper(args_cli.save_viz_dir) + + # Create constant downward force + force_tensor = torch.zeros(scene.num_envs, 1, 3, device=sim.device) + torque_tensor = torch.zeros(scene.num_envs, 1, 3, device=sim.device) + force_tensor[:, 0, 2] = -1.0 + + nrows = scene["tactile_sensor"].cfg.tactile_array_size[0] + ncols = scene["tactile_sensor"].cfg.tactile_array_size[1] + + physics_timer = Timer() + physics_total_time = 0.0 + physics_total_count = 0 + + entity_list = ["robot"] + if "contact_object" in scene.keys(): + entity_list.append("contact_object") + + while simulation_app.is_running(): + if count == 122: + # Reset robot and contact object positions + count = 0 + for entity in entity_list: + root_state = scene[entity].data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + scene[entity].write_root_state_to_sim(root_state) + + scene.reset() + print("[INFO]: Resetting robot and contact object state...") + + if "contact_object" in scene.keys(): + # rotation + if count > 20: + env_indices = torch.arange(scene.num_envs, device=sim.device) + odd_mask = env_indices % 2 == 1 + even_mask = env_indices % 2 == 0 + torque_tensor[odd_mask, 0, 2] = 10 # rotation for odd environments + torque_tensor[even_mask, 0, 2] = -10 # rotation for even environments + scene["contact_object"].permanent_wrench_composer.set_forces_and_torques(force_tensor, torque_tensor) + + # Step simulation + scene.write_data_to_sim() + physics_timer.start() + sim.step() + physics_timer.stop() + physics_total_time += physics_timer.total_run_time + physics_total_count += 1 + sim_time += sim_dt + count += 1 + scene.update(sim_dt) + + # Access tactile sensor data + tactile_data = scene["tactile_sensor"].data + + if args_cli.save_viz: + save_viz_helper(dir_path_list, count, tactile_data, num_envs, nrows, ncols) + + # Get timing summary from sensor and add physics timing + timing_summary = scene["tactile_sensor"].get_timing_summary() + + # Add physics timing to the summary + physics_avg = physics_total_time / (physics_total_count * scene.num_envs) if physics_total_count > 0 else 0.0 + timing_summary["physics_total"] = physics_total_time + timing_summary["physics_average"] = physics_avg + timing_summary["physics_fps"] = 1 / physics_avg if physics_avg > 0 else 0.0 + + print(timing_summary) + + +def main(): + """Main function.""" + # Initialize simulation + # Note: We set the gpu_collision_stack_size to prevent buffer overflow in contact-rich environments. + sim_cfg = sim_utils.SimulationCfg( + dt=0.005, + device=args_cli.device, + physx=sim_utils.PhysxCfg(gpu_collision_stack_size=2**30), + ) + sim = sim_utils.SimulationContext(sim_cfg) + + # Set main camera + sim.set_camera_view(eye=[0.5, 0.6, 1.0], target=[-0.1, 0.1, 0.5]) + + # Create scene based on contact object type + if args_cli.contact_object_type == "cube": + scene_cfg = CubeTactileSceneCfg(num_envs=args_cli.num_envs, env_spacing=0.2) + # disabled force field for cube contact object because a SDF collision mesh cannot + # be created for the Shape Prims + scene_cfg.tactile_sensor.enable_force_field = False + elif args_cli.contact_object_type == "nut": + scene_cfg = NutTactileSceneCfg(num_envs=args_cli.num_envs, env_spacing=0.2) + elif args_cli.contact_object_type == "none": + scene_cfg = TactileSensorsSceneCfg(num_envs=args_cli.num_envs, env_spacing=0.2) + scene_cfg.tactile_sensor.contact_object_prim_path_expr = None + # this flag is to visualize the tactile sensor points + scene_cfg.tactile_sensor.debug_vis = True + else: + raise ValueError( + f"Invalid contact object type: '{args_cli.contact_object_type}'. Must be 'none', 'cube', or 'nut'." + ) + + scene = InteractiveScene(scene_cfg) + + # Initialize simulation + sim.reset() + print("[INFO]: Setup complete...") + + # Get initial render + scene["tactile_sensor"].get_initial_render() + # Run simulation + run_simulator(sim, scene) + + +if __name__ == "__main__": + # Run the main function + main() + # Close sim app + simulation_app.close() diff --git a/scripts/environments/export_IODescriptors.py b/scripts/environments/export_IODescriptors.py index d89e607e3bdf..3f515a166f98 100644 --- a/scripts/environments/export_IODescriptors.py +++ b/scripts/environments/export_IODescriptors.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/environments/list_envs.py b/scripts/environments/list_envs.py index f9352dc469f8..0beb83e92131 100644 --- a/scripts/environments/list_envs.py +++ b/scripts/environments/list_envs.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +15,16 @@ """Launch Isaac Sim Simulator first.""" +import argparse + from isaaclab.app import AppLauncher +# add argparse arguments +parser = argparse.ArgumentParser(description="List Isaac Lab environments.") +parser.add_argument("--keyword", type=str, default=None, help="Keyword to filter environments.") +# parse the arguments +args_cli = parser.parse_args() + # launch omniverse app app_launcher = AppLauncher(headless=True) simulation_app = app_launcher.app @@ -44,7 +52,7 @@ def main(): index = 0 # acquire all Isaac environments names for task_spec in gym.registry.values(): - if "Isaac" in task_spec.id: + if "Isaac" in task_spec.id and (args_cli.keyword is None or args_cli.keyword 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 diff --git a/scripts/environments/random_agent.py b/scripts/environments/random_agent.py index b3187c3b3729..6a40060d64b1 100644 --- a/scripts/environments/random_agent.py +++ b/scripts/environments/random_agent.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/environments/state_machine/lift_cube_sm.py b/scripts/environments/state_machine/lift_cube_sm.py index 330fb25e4dfb..6136e2e3a351 100644 --- a/scripts/environments/state_machine/lift_cube_sm.py +++ b/scripts/environments/state_machine/lift_cube_sm.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -38,10 +38,10 @@ """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 diff --git a/scripts/environments/state_machine/lift_teddy_bear.py b/scripts/environments/state_machine/lift_teddy_bear.py index 5d6db4153903..2eb4ae710099 100644 --- a/scripts/environments/state_machine/lift_teddy_bear.py +++ b/scripts/environments/state_machine/lift_teddy_bear.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +40,10 @@ """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 diff --git a/scripts/environments/state_machine/open_cabinet_sm.py b/scripts/environments/state_machine/open_cabinet_sm.py index 9c644254008d..3cb88d31a1a2 100644 --- a/scripts/environments/state_machine/open_cabinet_sm.py +++ b/scripts/environments/state_machine/open_cabinet_sm.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -38,10 +38,10 @@ """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 diff --git a/scripts/environments/teleoperation/teleop_se3_agent.py b/scripts/environments/teleoperation/teleop_se3_agent.py index 32f125b194fa..8492ad77f3ce 100644 --- a/scripts/environments/teleoperation/teleop_se3_agent.py +++ b/scripts/environments/teleoperation/teleop_se3_agent.py @@ -1,9 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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).""" """Launch Isaac Sim Simulator first.""" @@ -13,7 +17,7 @@ from isaaclab.app import AppLauncher # 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", @@ -55,14 +59,15 @@ """Rest everything follows.""" +import logging + import gymnasium as gym import torch -import omni.log - 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 @@ -73,10 +78,13 @@ import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 +# import logger +logger = logging.getLogger(__name__) + def main() -> None: """ - Run keyboard teleoperation with Isaac Lab manipulation environment. + 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,6 +95,11 @@ def main() -> None: # parse configuration env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, 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: @@ -96,8 +109,6 @@ def main() -> None: env_cfg.terminations.object_reached_goal = DoneTerm(func=mdp.object_reached_goal) 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" @@ -106,12 +117,12 @@ def main() -> None: env = gym.make(args_cli.task, cfg=env_cfg).unwrapped # check environment name (for reach , we don't allow the gripper) if "Reach" in args_cli.task: - omni.log.warn( + logger.warning( f"The environment '{args_cli.task}' does not support gripper control. The device command will be" " ignored." ) except Exception as e: - omni.log.error(f"Failed to create environment: {e}") + logger.error(f"Failed to create environment: {e}") simulation_app.close() return @@ -183,7 +194,9 @@ def stop_teleoperation() -> None: args_cli.teleop_device, env_cfg.teleop_devices.devices, teleoperation_callbacks ) else: - omni.log.warn(f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default.") + logger.warning( + f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default." + ) # Create fallback teleop device sensitivity = args_cli.sensitivity if args_cli.teleop_device.lower() == "keyboard": @@ -199,8 +212,8 @@ def stop_teleoperation() -> None: Se3GamepadCfg(pos_sensitivity=0.1 * sensitivity, rot_sensitivity=0.1 * sensitivity) ) else: - omni.log.error(f"Unsupported teleop device: {args_cli.teleop_device}") - omni.log.error("Supported devices: keyboard, spacemouse, gamepad, handtracking") + logger.error(f"Unsupported teleop device: {args_cli.teleop_device}") + logger.error("Configure the teleop device in the environment config.") env.close() simulation_app.close() return @@ -210,15 +223,15 @@ def stop_teleoperation() -> None: try: teleop_interface.add_callback(key, callback) except (ValueError, TypeError) as e: - omni.log.warn(f"Failed to add callback for key {key}: {e}") + logger.warning(f"Failed to add callback for key {key}: {e}") except Exception as e: - omni.log.error(f"Failed to create teleop device: {e}") + logger.error(f"Failed to create teleop device: {e}") env.close() simulation_app.close() return if teleop_interface is None: - omni.log.error("Failed to create teleop interface") + logger.error("Failed to create teleop interface") env.close() simulation_app.close() return @@ -250,10 +263,11 @@ def stop_teleoperation() -> None: if should_reset_recording_instance: env.reset() + teleop_interface.reset() should_reset_recording_instance = False print("Environment reset complete") except Exception as e: - omni.log.error(f"Error during simulation step: {e}") + logger.error(f"Error during simulation step: {e}") break # close the simulator diff --git a/scripts/environments/zero_agent.py b/scripts/environments/zero_agent.py index c66b075bbc78..edd9317a6287 100644 --- a/scripts/environments/zero_agent.py +++ b/scripts/environments/zero_agent.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/imitation_learning/isaaclab_mimic/annotate_demos.py b/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py index 17322c6e93ce..a60f7913549f 100644 --- a/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py +++ b/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -47,7 +47,8 @@ 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 + # 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 @@ -58,8 +59,9 @@ """Rest everything follows.""" import contextlib -import gymnasium as gym import os + +import gymnasium as gym import torch import isaaclab_mimic.envs # noqa: F401 diff --git a/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py b/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py index 7810639947fb..d180dffd7ccf 100644 --- a/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py +++ b/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -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 @@ -301,7 +302,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 diff --git a/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py b/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py index a260151f4b15..527792ea9038 100644 --- a/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py +++ b/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -7,7 +7,6 @@ Main data generation script. """ - """Launch Isaac Sim Simulator first.""" import argparse @@ -51,7 +50,8 @@ 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 + # 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 @@ -62,13 +62,13 @@ """Rest everything follows.""" import asyncio -import gymnasium as gym import inspect -import numpy as np +import logging import random -import torch -import omni +import gymnasium as gym +import numpy as np +import torch from isaaclab.envs import ManagerBasedRLMimicEnv @@ -76,11 +76,15 @@ 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 logger +logger = logging.getLogger(__name__) + def main(): num_envs = args_cli.num_envs @@ -110,7 +114,7 @@ def main(): # Check if the mimic API from this environment contains decprecated signatures if "action_noise_dict" not in inspect.signature(env.target_eef_pose_to_action).parameters: - omni.log.warn( + logger.warning( f'The "noise" parameter in the "{env_name}" environment\'s mimic API "target_eef_pose_to_action", ' "is deprecated. Please update the API to take action_noise_dict instead." ) diff --git a/scripts/imitation_learning/locomanipulation_sdg/generate_data.py b/scripts/imitation_learning/locomanipulation_sdg/generate_data.py index b80e0992ce41..4999f2d3fefc 100644 --- a/scripts/imitation_learning/locomanipulation_sdg/generate_data.py +++ b/scripts/imitation_learning/locomanipulation_sdg/generate_data.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -102,7 +102,8 @@ 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 + # 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 @@ -110,8 +111,9 @@ simulation_app = app_launcher.app import enum -import gymnasium as gym import random + +import gymnasium as gym import torch import omni.kit @@ -263,10 +265,12 @@ def setup_navigation_scene( Tuple of (occupancy_map, path_helper, base_goal, base_goal_approach) """ # 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(), - ]) + 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: @@ -672,7 +676,6 @@ def replay( # 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}") # Execute state-specific logic using helper functions @@ -723,9 +726,7 @@ def replay( if __name__ == "__main__": - with torch.no_grad(): - # Create environment if args_cli.task is not None: env_name = args_cli.task.split(":")[-1] @@ -744,7 +745,6 @@ def replay( input_dataset_file_handler.open(args_cli.dataset) for i in range(args_cli.num_runs): - if args_cli.demo is None: demo = random.choice(list(input_dataset_file_handler.get_episode_names())) else: diff --git a/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py b/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py index 6981ff803d17..e65059d7d65a 100644 --- a/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py +++ b/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +15,10 @@ """ import argparse +import os + import h5py import matplotlib.pyplot as plt -import os def main(): diff --git a/scripts/imitation_learning/robomimic/play.py b/scripts/imitation_learning/robomimic/play.py index 4cc327941d0d..f663bc3acb2b 100644 --- a/scripts/imitation_learning/robomimic/play.py +++ b/scripts/imitation_learning/robomimic/play.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -49,7 +49,8 @@ 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 + # 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 @@ -60,17 +61,17 @@ """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 +import torch 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.manager_based.manipulation.pick_place # noqa: F401 from isaaclab_tasks.utils import parse_env_cfg diff --git a/scripts/imitation_learning/robomimic/robust_eval.py b/scripts/imitation_learning/robomimic/robust_eval.py index c8660cc6696d..0e1e9014ba92 100644 --- a/scripts/imitation_learning/robomimic/robust_eval.py +++ b/scripts/imitation_learning/robomimic/robust_eval.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -65,7 +65,8 @@ 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 + # 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 @@ -76,14 +77,14 @@ """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 diff --git a/scripts/imitation_learning/robomimic/train.py b/scripts/imitation_learning/robomimic/train.py index 718a18bcbca1..11dd9814de6d 100644 --- a/scripts/imitation_learning/robomimic/train.py +++ b/scripts/imitation_learning/robomimic/train.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -53,36 +53,31 @@ """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 isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 @@ -369,7 +364,18 @@ def main(args: argparse.Namespace): f" Please check that the gym registry has the entry point: '{cfg_entry_point_key}'." ) - with open(cfg_entry_point_file) as f: + # resolve module path if needed + if ":" in cfg_entry_point_file: + mod_name, file_name = cfg_entry_point_file.split(":") + mod = importlib.import_module(mod_name) + if mod.__file__ is None: + raise ValueError(f"Could not find module file for: '{mod_name}'") + mod_path = os.path.dirname(mod.__file__) + config_file = os.path.join(mod_path, file_name) + else: + config_file = cfg_entry_point_file + + with open(config_file) as f: ext_cfg = json.load(f) config = config_factory(ext_cfg["algo_name"]) # update config with external json - this will throw errors if diff --git a/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py b/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py index b10083ee9f53..b7b3c5cf89e2 100644 --- a/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py +++ b/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 0a6889d075bf..f43ae7ecaaaa 100644 --- a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py +++ b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py @@ -1,9 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 import sys +from typing import Any # Allow for import of items from the ray workflow. CUR_DIR = pathlib.Path(__file__).parent @@ -12,6 +13,8 @@ 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): @@ -47,3 +50,36 @@ def __init__(self, cfg: dict = {}): cfg = util.populate_isaac_ray_cfg_args(cfg) cfg["runner_args"]["--task"] = tune.choice(["Isaac-Cartpole-RGB-TheiaTiny-v0"]) 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() + + def __call__(self, trial_id: str, result: dict[str, Any]) -> bool: + iter = result.get("training_iteration", 0) + out_of_bounds = result.get("Episode/Episode_Termination/cart_out_of_bounds") + + # Mark the trial for stopping if conditions are met + 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 + + def stop_all(self) -> bool: + return False # only stop individual trials diff --git a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py index e563cf281ee1..cb59a993368d 100644 --- a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py +++ b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -85,12 +85,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 +100,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 +141,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 ccf73be7fdf9..3a3be716702e 100644 --- a/scripts/reinforcement_learning/ray/launch.py +++ b/scripts/reinforcement_learning/ray/launch.py @@ -1,17 +1,8 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 64f3cb707ba2..2c45f1cd0a8d 100644 --- a/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py +++ b/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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): @@ -66,6 +66,7 @@ def process_run(args): def download_experiment_tensorboard_logs(uri: str, experiment_name: str, download_dir: str) -> None: """Download MLflow experiment logs and convert to TensorBoard format.""" + # import logger logger = logging.getLogger(__name__) try: diff --git a/scripts/reinforcement_learning/ray/submit_job.py b/scripts/reinforcement_learning/ray/submit_job.py index b02d92537e93..21fb6a3d9b39 100644 --- a/scripts/reinforcement_learning/ray/submit_job.py +++ b/scripts/reinforcement_learning/ray/submit_job.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 43e369ff218e..7bb3596fc918 100644 --- a/scripts/reinforcement_learning/ray/task_runner.py +++ b/scripts/reinforcement_learning/ray/task_runner.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +58,8 @@ python task_runner.py --task_cfg /path/to/tasks.yaml YAML configuration example-1: ---------------------------- +----------------------------- + .. code-block:: yaml pip: ["xxx"] @@ -70,7 +78,8 @@ memory: 10*1024*1024*1024 YAML configuration example-2: ---------------------------- +----------------------------- + .. code-block:: yaml pip: ["xxx"] @@ -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 f9be4b0eed64..99dc7e8d08f5 100644 --- a/scripts/reinforcement_learning/ray/tuner.py +++ b/scripts/reinforcement_learning/ray/tuner.py @@ -1,10 +1,11 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 importlib.util import os +import random import subprocess import sys from time import sleep, time @@ -12,8 +13,11 @@ import ray 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 """ This script breaks down an aggregate tuning job, as defined by a hyperparameter sweep configuration, @@ -45,6 +49,11 @@ ./isaaclab.sh -p 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 + ./isaaclab.sh -p 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 \ --aggregate_jobs tuner.py \ @@ -60,7 +69,7 @@ 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 -MAX_LOG_EXTRACTION_ERRORS = 2 # maximum allowed LogExtractionErrors before we abort the whole training +MAX_LOG_EXTRACTION_ERRORS = 10 # maximum allowed LogExtractionErrors before we abort the whole training class IsaacLabTuneTrainable(tune.Trainable): @@ -203,13 +212,40 @@ def stop_all(self): return False -def invoke_tuning_run(cfg: dict, args: argparse.Namespace) -> None: +class ProcessCleanupCallback(Callback): + """Callback to clean up processes when trials are stopped.""" + + def on_trial_error(self, iteration, trials, trial, error, **info): + """Called when a trial encounters an error.""" + self._cleanup_trial(trial) + + def on_trial_complete(self, iteration, trials, trial, **info): + """Called when a trial completes.""" + self._cleanup_trial(trial) + + def _cleanup_trial(self, trial): + """Clean up processes for a trial using SIGKILL.""" + try: + subprocess.run(["pkill", "-9", "-f", f"rid {trial.config['runner_args']['-rid']}"], check=False) + sleep(5) + except Exception as e: + print(f"[ERROR]: Failed to cleanup trial {trial.trial_id}: {e}") + + +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. Log either to a local directory or to MLFlow. 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 os.environ["TUNE_DISABLE_STRICT_METRIC_CHECKING"] = "1" @@ -217,17 +253,17 @@ def invoke_tuning_run(cfg: dict, args: argparse.Namespace) -> None: print("[WARNING]: Not saving checkpoints, just running experiment...") print("[INFO]: Model parameters and metrics will be preserved.") print("[WARNING]: For homogeneous cluster resources only...") + + # Initialize Ray + util.ray_init( + ray_address=args.ray_address, + log_to_driver=True, + ) + # Get available resources resources = util.get_gpu_node_resources() print(f"[INFO]: Available resources {resources}") - if not ray.is_initialized(): - ray.init( - address=args.ray_address, - log_to_driver=True, - num_gpus=len(resources), - ) - print(f"[INFO]: Using config {cfg}") # Configure the search algorithm and the repeater @@ -237,16 +273,37 @@ def invoke_tuning_run(cfg: dict, args: argparse.Namespace) -> None: ) 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 []), + ] + ) + + 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( storage_path="/tmp/ray", name=f"IsaacRay-{args.cfg_class}-tune", + callbacks=[ProcessCleanupCallback()], verbose=1, checkpoint_config=air.CheckpointConfig( checkpoint_frequency=0, # Disable periodic checkpointing checkpoint_at_end=False, # Disable final checkpoint ), - stop=LogExtractionErrorStopper(max_errors=MAX_LOG_EXTRACTION_ERRORS), + stop=stoppers, + progress_reporter=progress_reporter, ) elif args.run_mode == "remote": # MLFlow, to MLFlow server @@ -260,13 +317,15 @@ def invoke_tuning_run(cfg: dict, args: argparse.Namespace) -> None: run_config = ray.train.RunConfig( name="mlflow", storage_path="/tmp/ray", - callbacks=[mlflow_callback], + callbacks=[ProcessCleanupCallback(), mlflow_callback], checkpoint_config=ray.train.CheckpointConfig(checkpoint_frequency=0, checkpoint_at_end=False), - stop=LogExtractionErrorStopper(max_errors=MAX_LOG_EXTRACTION_ERRORS), + stop=stoppers, + progress_reporter=progress_reporter, ) else: raise ValueError("Unrecognized run mode.") - + # RID isn't optimized as it is sampled from, but useful for cleanup later + cfg["runner_args"]["-rid"] = tune.sample_from(lambda _: str(random.randint(int(1e9), int(1e10) - 1))) # Configure the tuning job tuner = tune.Tuner( IsaacLabTuneTrainable, @@ -399,6 +458,22 @@ 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, + default=None, + help="A stop criteria in the cfg_file, must be a tune.Stopper instance.", + ) args = parser.parse_args() PROCESS_RESPONSE_TIMEOUT = args.process_response_timeout @@ -457,7 +532,25 @@ def __init__(self, cfg: dict): print(f"[INFO]: Successfully instantiated class '{class_name}' from {file_path}") cfg = instance.cfg print(f"[INFO]: Grabbed the following hyperparameter sweep config: \n {cfg}") - invoke_tuning_run(cfg, args) + # Load optional stopper config + stopper = None + if args.stopper and hasattr(module, args.stopper): + stopper = getattr(module, args.stopper) + if isinstance(stopper, type) and issubclass(stopper, tune.Stopper): + stopper = stopper() + else: + raise TypeError(f"[ERROR]: Unsupported stop criteria type: {type(stopper)}") + print(f"[INFO]: Loaded custom stop criteria from '{args.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 427c887cdcc3..a73ebdf493dc 100644 --- a/scripts/reinforcement_learning/ray/util.py +++ b/scripts/reinforcement_learning/ray/util.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -71,7 +71,7 @@ def process_args(args, target_list, is_hydra=False): if not is_hydra: if key.endswith("_singleton"): target_list.append(value) - elif key.startswith("--"): + elif key.startswith("--") or key.startswith("-"): target_list.append(f"{key} {value}") # Space instead of = for runner args else: target_list.append(f"{value}") @@ -320,6 +320,8 @@ def ray_init(ray_address: str = "auto", runtime_env: dict[str, Any] | None = Non f" runtime_env={runtime_env}" ) ray.init(address=ray_address, runtime_env=runtime_env, log_to_driver=log_to_driver) + else: + print("[WARNING]: Attempting to initialize Ray but it is already initialized!") def get_gpu_node_resources( @@ -343,7 +345,7 @@ def get_gpu_node_resources( or simply the resource for a single node if requested. """ if not ray.is_initialized(): - ray_init() + raise RuntimeError("Ray must be initialized before calling get_gpu_node_resources().") nodes = ray.nodes() node_resources = [] total_cpus = 0 diff --git a/scripts/reinforcement_learning/ray/wrap_resources.py b/scripts/reinforcement_learning/ray/wrap_resources.py index 75333ddcde07..158bd0d82460 100644 --- a/scripts/reinforcement_learning/ray/wrap_resources.py +++ b/scripts/reinforcement_learning/ray/wrap_resources.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/reinforcement_learning/rl_games/play.py b/scripts/reinforcement_learning/rl_games/play.py index d6faec373160..ee2dbcdbb149 100644 --- a/scripts/reinforcement_learning/rl_games/play.py +++ b/scripts/reinforcement_learning/rl_games/play.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -54,13 +54,13 @@ """Rest everything follows.""" -import gymnasium as gym import math import os import random import time -import torch +import gymnasium as gym +import torch from rl_games.common import env_configurations, vecenv from rl_games.common.player import BasePlayer from rl_games.torch_runner import Runner @@ -74,9 +74,9 @@ ) from isaaclab.utils.assets import retrieve_file_path from isaaclab.utils.dict import print_dict -from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint from isaaclab_rl.rl_games import RlGamesGpuEnv, RlGamesVecEnvWrapper +from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils import get_checkpoint_path @@ -95,10 +95,6 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # override configurations with non-hydra 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 - # update agent device to match simulation device - if args_cli.device is not None: - agent_cfg["params"]["config"]["device"] = args_cli.device - agent_cfg["params"]["config"]["device_name"] = args_cli.device # randomly sample a seed if seed = -1 if args_cli.seed == -1: diff --git a/scripts/reinforcement_learning/rl_games/train.py b/scripts/reinforcement_learning/rl_games/train.py index f4952df5f2f6..5b85ba5b429d 100644 --- a/scripts/reinforcement_learning/rl_games/train.py +++ b/scripts/reinforcement_learning/rl_games/train.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -42,6 +42,9 @@ help="if toggled, this experiment will be tracked with Weights and Biases", ) parser.add_argument("--export_io_descriptors", action="store_true", default=False, help="Export IO descriptors.") +parser.add_argument( + "--ray-proc-id", "-rid", type=int, default=None, help="Automatically configured by Ray integration, otherwise None." +) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments @@ -59,13 +62,14 @@ """Rest everything follows.""" -import gymnasium as gym +import logging import math import os import random +import time from datetime import datetime -import omni +import gymnasium as gym from rl_games.common import env_configurations, vecenv from rl_games.common.algo_observer import IsaacAlgoObserver from rl_games.torch_runner import Runner @@ -86,6 +90,9 @@ import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.hydra import hydra_task_config +# import logger +logger = logging.getLogger(__name__) + # PLACEHOLDER: Extension template (do not remove this comment) @@ -102,11 +109,6 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen "Please use GPU device (e.g., --device cuda) for distributed training." ) - # update agent device to match simulation device - if args_cli.device is not None: - agent_cfg["params"]["config"]["device"] = args_cli.device - agent_cfg["params"]["config"]["device_name"] = args_cli.device - # randomly sample a seed if seed = -1 if args_cli.seed == -1: args_cli.seed = random.randint(0, 10000) @@ -138,11 +140,10 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # specify directory for logging experiments config_name = agent_cfg["params"]["config"]["name"] log_root_path = os.path.join("logs", "rl_games", config_name) - if "pbt" in agent_cfg: - if agent_cfg["pbt"]["directory"] == ".": - log_root_path = os.path.abspath(log_root_path) - else: - log_root_path = os.path.join(agent_cfg["pbt"]["directory"], log_root_path) + if "pbt" in agent_cfg and agent_cfg["pbt"]["directory"] != ".": + log_root_path = os.path.join(agent_cfg["pbt"]["directory"], log_root_path) + else: + log_root_path = os.path.abspath(log_root_path) print(f"[INFO] Logging experiment in directory: {log_root_path}") # specify directory for logging runs @@ -157,6 +158,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # dump the configuration into log-directory dump_yaml(os.path.join(log_root_path, log_dir, "params", "env.yaml"), env_cfg) dump_yaml(os.path.join(log_root_path, log_dir, "params", "agent.yaml"), agent_cfg) + print(f"Exact experiment name requested from command line: {os.path.join(log_root_path, log_dir)}") # read configurations about the agent-training rl_device = agent_cfg["params"]["config"]["device"] @@ -169,7 +171,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors else: - omni.log.warn( + logger.warning( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) @@ -195,6 +197,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen print_dict(video_kwargs, nesting=4) env = gym.wrappers.RecordVideo(env, **video_kwargs) + start_time = time.time() + # wrap around environment for rl-games env = RlGamesVecEnvWrapper(env, rl_device, clip_obs, clip_actions, obs_groups, concate_obs_groups) @@ -244,6 +248,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen else: runner.run({"train": True, "play": False, "sigma": train_sigma}) + print(f"Training time: {round(time.time() - start_time, 2)} seconds") + # close the simulator env.close() diff --git a/scripts/reinforcement_learning/rsl_rl/cli_args.py b/scripts/reinforcement_learning/rsl_rl/cli_args.py index c176f774515c..51cf868b5cd5 100644 --- a/scripts/reinforcement_learning/rsl_rl/cli_args.py +++ b/scripts/reinforcement_learning/rsl_rl/cli_args.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/reinforcement_learning/rsl_rl/play.py b/scripts/reinforcement_learning/rsl_rl/play.py index 11ef73994620..beb920721738 100644 --- a/scripts/reinforcement_learning/rsl_rl/play.py +++ b/scripts/reinforcement_learning/rsl_rl/play.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -53,11 +53,11 @@ """Rest everything follows.""" -import gymnasium as gym import os import time -import torch +import gymnasium as gym +import torch from rsl_rl.runners import DistillationRunner, OnPolicyRunner from isaaclab.envs import ( @@ -69,9 +69,9 @@ ) from isaaclab.utils.assets import retrieve_file_path from isaaclab.utils.dict import print_dict -from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint from isaaclab_rl.rsl_rl import RslRlBaseRunnerCfg, RslRlVecEnvWrapper, export_policy_as_jit, export_policy_as_onnx +from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils import get_checkpoint_path @@ -185,7 +185,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # agent stepping actions = policy(obs) # env stepping - obs, _, _, _ = env.step(actions) + obs, _, dones, _ = env.step(actions) + # reset recurrent states for episodes that have terminated + policy_nn.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 8b66feb28aaa..0cce12d7eba0 100644 --- a/scripts/reinforcement_learning/rsl_rl/train.py +++ b/scripts/reinforcement_learning/rsl_rl/train.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +31,9 @@ "--distributed", action="store_true", default=False, help="Run training with multiple GPUs or nodes." ) parser.add_argument("--export_io_descriptors", action="store_true", default=False, help="Export IO descriptors.") +parser.add_argument( + "--ray-proc-id", "-rid", type=int, default=None, help="Automatically configured by Ray integration, otherwise None." +) # append RSL-RL cli arguments cli_args.add_rsl_rl_args(parser) # append AppLauncher cli args @@ -72,12 +75,13 @@ """Rest everything follows.""" -import gymnasium as gym +import logging import os -import torch +import time from datetime import datetime -import omni +import gymnasium as gym +import torch from rsl_rl.runners import DistillationRunner, OnPolicyRunner from isaaclab.envs import ( @@ -96,6 +100,9 @@ from isaaclab_tasks.utils import get_checkpoint_path from isaaclab_tasks.utils.hydra import hydra_task_config +# import logger +logger = logging.getLogger(__name__) + # PLACEHOLDER: Extension template (do not remove this comment) torch.backends.cuda.matmul.allow_tf32 = True @@ -141,7 +148,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen print(f"[INFO] Logging experiment in directory: {log_root_path}") # specify directory for logging runs: {time-stamp}_{run_name} log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - # The Ray Tune workflow extracts experiment name using the logging line below, hence, do not change it (see PR #2346, comment-2819298849) + # The Ray Tune workflow extracts experiment name using the logging line below, hence, do not + # change it (see PR #2346, comment-2819298849) print(f"Exact experiment name requested from command line: {log_dir}") if agent_cfg.run_name: log_dir += f"_{agent_cfg.run_name}" @@ -151,7 +159,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors else: - omni.log.warn( + logger.warning( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) @@ -181,6 +189,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen print_dict(video_kwargs, nesting=4) env = gym.wrappers.RecordVideo(env, **video_kwargs) + start_time = time.time() + # wrap around environment for rsl-rl env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions) @@ -206,6 +216,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # run training runner.learn(num_learning_iterations=agent_cfg.max_iterations, init_at_random_ep_len=True) + print(f"Training time: {round(time.time() - start_time, 2)} seconds") + # close the simulator env.close() diff --git a/scripts/reinforcement_learning/sb3/play.py b/scripts/reinforcement_learning/sb3/play.py index c803c1807baf..4afe943f62fd 100644 --- a/scripts/reinforcement_learning/sb3/play.py +++ b/scripts/reinforcement_learning/sb3/play.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -61,12 +61,12 @@ """Rest everything follows.""" -import gymnasium as gym import os import random import time -import torch +import gymnasium as gym +import torch from stable_baselines3 import PPO from stable_baselines3.common.vec_env import VecNormalize @@ -78,9 +78,9 @@ multi_agent_to_single_agent, ) from isaaclab.utils.dict import print_dict -from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint from isaaclab_rl.sb3 import Sb3VecEnvWrapper, process_sb3_cfg +from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.hydra import hydra_task_config diff --git a/scripts/reinforcement_learning/sb3/train.py b/scripts/reinforcement_learning/sb3/train.py index be43b3b8ac8b..32549dcd4ea3 100644 --- a/scripts/reinforcement_learning/sb3/train.py +++ b/scripts/reinforcement_learning/sb3/train.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -37,6 +37,9 @@ default=False, help="Use a slower SB3 wrapper but keep all the extra training info.", ) +parser.add_argument( + "--ray-proc-id", "-rid", type=int, default=None, help="Automatically configured by Ray integration, otherwise None." +) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments @@ -72,13 +75,14 @@ def cleanup_pbar(*args): """Rest everything follows.""" -import gymnasium as gym -import numpy as np +import logging import os import random +import time from datetime import datetime -import omni +import gymnasium as gym +import numpy as np from stable_baselines3 import PPO from stable_baselines3.common.callbacks import CheckpointCallback, LogEveryNTimesteps from stable_baselines3.common.vec_env import VecNormalize @@ -98,6 +102,8 @@ def cleanup_pbar(*args): import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.hydra import hydra_task_config +# import logger +logger = logging.getLogger(__name__) # PLACEHOLDER: Extension template (do not remove this comment) @@ -124,7 +130,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen run_info = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") log_root_path = os.path.abspath(os.path.join("logs", "sb3", args_cli.task)) print(f"[INFO] Logging experiment in directory: {log_root_path}") - # The Ray Tune workflow extracts experiment name using the logging line below, hence, do not change it (see PR #2346, comment-2819298849) + # The Ray Tune workflow extracts experiment name using the logging line below, hence, + # do not change it (see PR #2346, comment-2819298849) print(f"Exact experiment name requested from command line: {run_info}") log_dir = os.path.join(log_root_path, run_info) # dump the configuration into log-directory @@ -145,7 +152,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors else: - omni.log.warn( + logger.warning( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) @@ -171,6 +178,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen print_dict(video_kwargs, nesting=4) env = gym.wrappers.RecordVideo(env, **video_kwargs) + start_time = time.time() + # wrap around environment for stable baselines env = Sb3VecEnvWrapper(env, fast_variant=not args_cli.keep_all_info) @@ -218,6 +227,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen print("Saving normalization") env.save(os.path.join(log_dir, "model_vecnormalize.pkl")) + print(f"Training time: {round(time.time() - start_time, 2)} seconds") + # close the simulator env.close() diff --git a/scripts/reinforcement_learning/skrl/play.py b/scripts/reinforcement_learning/skrl/play.py index 6be6b0eae3b4..089ec7561979 100644 --- a/scripts/reinforcement_learning/skrl/play.py +++ b/scripts/reinforcement_learning/skrl/play.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -74,13 +74,13 @@ """Rest everything follows.""" -import gymnasium as gym import os import random import time -import torch +import gymnasium as gym import skrl +import torch from packaging import version # check for minimum supported skrl version @@ -105,9 +105,9 @@ multi_agent_to_single_agent, ) from isaaclab.utils.dict import print_dict -from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint from isaaclab_rl.skrl import SkrlVecEnvWrapper +from isaaclab_rl.utils.pretrained_checkpoint import get_published_pretrained_checkpoint import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils import get_checkpoint_path diff --git a/scripts/reinforcement_learning/skrl/train.py b/scripts/reinforcement_learning/skrl/train.py index d73a2a402629..cf2edce47435 100644 --- a/scripts/reinforcement_learning/skrl/train.py +++ b/scripts/reinforcement_learning/skrl/train.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -54,7 +54,9 @@ choices=["AMP", "PPO", "IPPO", "MAPPO"], help="The RL algorithm used for training the skrl agent.", ) - +parser.add_argument( + "--ray-proc-id", "-rid", type=int, default=None, help="Automatically configured by Ray integration, otherwise None." +) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments @@ -72,12 +74,13 @@ """Rest everything follows.""" -import gymnasium as gym +import logging import os import random +import time from datetime import datetime -import omni +import gymnasium as gym import skrl from packaging import version @@ -111,6 +114,9 @@ import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.hydra import hydra_task_config +# import logger +logger = logging.getLogger(__name__) + # PLACEHOLDER: Extension template (do not remove this comment) # config shortcuts @@ -162,10 +168,11 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen print(f"[INFO] Logging experiment in directory: {log_root_path}") # specify directory for logging runs: {time-stamp}_{run_name} log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + f"_{algorithm}_{args_cli.ml_framework}" - # The Ray Tune workflow extracts experiment name using the logging line below, hence, do not change it (see PR #2346, comment-2819298849) + # The Ray Tune workflow extracts experiment name using the logging line below, hence, + # do not change it (see PR #2346, comment-2819298849) print(f"Exact experiment name requested from command line: {log_dir}") if agent_cfg["agent"]["experiment"]["experiment_name"]: - log_dir += f'_{agent_cfg["agent"]["experiment"]["experiment_name"]}' + log_dir += f"_{agent_cfg['agent']['experiment']['experiment_name']}" # set directory into agent config agent_cfg["agent"]["experiment"]["directory"] = log_root_path agent_cfg["agent"]["experiment"]["experiment_name"] = log_dir @@ -183,7 +190,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors else: - omni.log.warn( + logger.warning( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) @@ -209,6 +216,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen print_dict(video_kwargs, nesting=4) env = gym.wrappers.RecordVideo(env, **video_kwargs) + start_time = time.time() + # wrap around environment for skrl env = SkrlVecEnvWrapper(env, ml_framework=args_cli.ml_framework) # same as: `wrap_env(env, wrapper="auto")` @@ -224,6 +233,8 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # run training runner.run() + print(f"Training time: {round(time.time() - start_time, 2)} seconds") + # close the simulator env.close() diff --git a/scripts/sim2sim_transfer/config/newton_to_physx_anymal_d.yaml b/scripts/sim2sim_transfer/config/newton_to_physx_anymal_d.yaml index bbf4b73dccb1..00d2925345b1 100644 --- a/scripts/sim2sim_transfer/config/newton_to_physx_anymal_d.yaml +++ b/scripts/sim2sim_transfer/config/newton_to_physx_anymal_d.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/sim2sim_transfer/config/newton_to_physx_g1.yaml b/scripts/sim2sim_transfer/config/newton_to_physx_g1.yaml index 3a2343405f3d..839980c4d10e 100644 --- a/scripts/sim2sim_transfer/config/newton_to_physx_g1.yaml +++ b/scripts/sim2sim_transfer/config/newton_to_physx_g1.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/sim2sim_transfer/config/newton_to_physx_go2.yaml b/scripts/sim2sim_transfer/config/newton_to_physx_go2.yaml index 143ca36d7992..d9f976ee1bb6 100644 --- a/scripts/sim2sim_transfer/config/newton_to_physx_go2.yaml +++ b/scripts/sim2sim_transfer/config/newton_to_physx_go2.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/sim2sim_transfer/config/newton_to_physx_h1.yaml b/scripts/sim2sim_transfer/config/newton_to_physx_h1.yaml index b88ee333cffa..cb0e0996f054 100644 --- a/scripts/sim2sim_transfer/config/newton_to_physx_h1.yaml +++ b/scripts/sim2sim_transfer/config/newton_to_physx_h1.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/sim2sim_transfer/rsl_rl_transfer.py b/scripts/sim2sim_transfer/rsl_rl_transfer.py index d35d57c6224f..0ec1b389879f 100644 --- a/scripts/sim2sim_transfer/rsl_rl_transfer.py +++ b/scripts/sim2sim_transfer/rsl_rl_transfer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -57,12 +57,12 @@ """Rest everything follows.""" -import gymnasium as gym import os import time + +import gymnasium as gym import torch import yaml - from rsl_rl.runners import DistillationRunner, OnPolicyRunner from isaaclab.envs import ( @@ -123,9 +123,9 @@ def get_joint_mappings(args_cli, action_space_dim): else: raise ValueError(f"Joint '{joint_name}' not found in source joint names") print(f"[INFO] Loaded joint mapping for policy transfer from YAML: {args_cli.policy_transfer_file}") - assert ( - len(source_to_target) == len(target_to_source) == num_joints - ), "Number of source and target joints must match" + assert len(source_to_target) == len(target_to_source) == num_joints, ( + "Number of source and target joints must match" + ) else: # Use identity mapping (one-to-one) identity_map = list(range(num_joints)) diff --git a/scripts/tools/blender_obj.py b/scripts/tools/blender_obj.py index 1597d800584f..c03a525fae49 100644 --- a/scripts/tools/blender_obj.py +++ b/scripts/tools/blender_obj.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 a18c22074046..d9ce51497d1a 100644 --- a/scripts/tools/check_instanceable.py +++ b/scripts/tools/check_instanceable.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -64,12 +64,10 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -from isaacsim.core.api.simulation_context import SimulationContext 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 @@ -80,12 +78,10 @@ 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() + stage = sim_utils.get_current_stage() # enable fabric which avoids passing data over to USD structure # this speeds up the read-write operation of GPU buffers @@ -96,17 +92,17 @@ def main(): sim.get_physics_context().set_gpu_total_aggregate_pairs_capacity(2**21) # 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._settings.set_bool("/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)) + sim_utils.create_prim("/World/envs/env_0/Asset", "Xform", usd_path=os.path.abspath(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 d1e335916372..7713bdc728f3 100644 --- a/scripts/tools/convert_instanceable.py +++ b/scripts/tools/convert_instanceable.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 bce2c66ef714..6e9fd46befd3 100644 --- a/scripts/tools/convert_mesh.py +++ b/scripts/tools/convert_mesh.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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) """ @@ -93,9 +94,9 @@ 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 @@ -187,7 +188,7 @@ def main(): # Simulate scene (if not headless) if local_gui or livestream_gui: # Open the stage with USD - stage_utils.open_stage(mesh_converter.usd_path) + sim_utils.open_stage(mesh_converter.usd_path) # Reinitialize the simulation app = omni.kit.app.get_app_interface() # Run simulation diff --git a/scripts/tools/convert_mjcf.py b/scripts/tools/convert_mjcf.py index 5b56bacaf0d4..40e46b82d59d 100644 --- a/scripts/tools/convert_mjcf.py +++ b/scripts/tools/convert_mjcf.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -63,9 +63,9 @@ 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 MjcfConverter, MjcfConverterCfg from isaaclab.utils.assets import check_file_path from isaaclab.utils.dict import print_dict @@ -122,7 +122,7 @@ def main(): # Simulate scene (if not headless) if local_gui or livestream_gui: # Open the stage with USD - stage_utils.open_stage(mjcf_converter.usd_path) + sim_utils.open_stage(mjcf_converter.usd_path) # Reinitialize the simulation app = omni.kit.app.get_app_interface() # Run simulation diff --git a/scripts/tools/convert_urdf.py b/scripts/tools/convert_urdf.py index 767d61b0a214..7d7a74708c59 100644 --- a/scripts/tools/convert_urdf.py +++ b/scripts/tools/convert_urdf.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -81,9 +81,9 @@ 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 UrdfConverter, UrdfConverterCfg from isaaclab.utils.assets import check_file_path from isaaclab.utils.dict import print_dict @@ -146,7 +146,7 @@ def main(): # Simulate scene (if not headless) if local_gui or livestream_gui: # Open the stage with USD - stage_utils.open_stage(urdf_converter.usd_path) + sim_utils.open_stage(urdf_converter.usd_path) # Reinitialize the simulation app = omni.kit.app.get_app_interface() # Run simulation diff --git a/scripts/tools/cosmos/cosmos_prompt_gen.py b/scripts/tools/cosmos/cosmos_prompt_gen.py index 673ae50ae149..32db884adc56 100644 --- a/scripts/tools/cosmos/cosmos_prompt_gen.py +++ b/scripts/tools/cosmos/cosmos_prompt_gen.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 e06f12178f74..0cd8a40c78f2 100644 --- a/scripts/tools/hdf5_to_mp4.py +++ b/scripts/tools/hdf5_to_mp4.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,20 @@ --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) """ -# 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 diff --git a/scripts/tools/merge_hdf5_datasets.py b/scripts/tools/merge_hdf5_datasets.py index 93d71b4599f0..a9fe1c63561d 100644 --- a/scripts/tools/merge_hdf5_datasets.py +++ b/scripts/tools/merge_hdf5_datasets.py @@ -1,12 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 e90804f12bc8..61f7b5b0b40b 100644 --- a/scripts/tools/mp4_to_hdf5.py +++ b/scripts/tools/mp4_to_hdf5.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/process_meshes_to_obj.py b/scripts/tools/process_meshes_to_obj.py index 412e753b807b..2c5be04c0e5c 100644 --- a/scripts/tools/process_meshes_to_obj.py +++ b/scripts/tools/process_meshes_to_obj.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 4eeef711a1c5..6bb8dea57073 100644 --- a/scripts/tools/record_demos.py +++ b/scripts/tools/record_demos.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,7 +18,8 @@ --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.""" @@ -75,7 +76,8 @@ 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 + # 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(): @@ -89,13 +91,13 @@ # Third-party imports -import gymnasium as gym +import logging import os import time + +import gymnasium as gym import torch -# Omniverse logger -import omni.log import omni.ui as ui from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg, Se3SpaceMouse, Se3SpaceMouseCfg @@ -106,8 +108,8 @@ 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 + import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 from collections.abc import Callable @@ -119,6 +121,9 @@ import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.parse_cfg import parse_env_cfg +# import logger +logger = logging.getLogger(__name__) + class RateLimiter: """Convenience class for enforcing rates in loops.""" @@ -201,7 +206,7 @@ def create_environment_config( env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1) env_cfg.env_name = args_cli.task.split(":")[-1] except Exception as e: - omni.log.error(f"Failed to parse environment configuration: {e}") + logger.error(f"Failed to parse environment configuration: {e}") exit(1) # extract success checking function to invoke in the main loop @@ -210,7 +215,7 @@ def create_environment_config( success_term = env_cfg.terminations.success env_cfg.terminations.success = None else: - omni.log.warn( + logger.warning( "No success termination term was found in the environment." " Will not be able to mark recorded demos as successful." ) @@ -251,7 +256,7 @@ def create_environment(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg) -> gym.En env = gym.make(args_cli.task, cfg=env_cfg).unwrapped return env except Exception as e: - omni.log.error(f"Failed to create environment: {e}") + logger.error(f"Failed to create environment: {e}") exit(1) @@ -276,26 +281,28 @@ def setup_teleop_device(callbacks: dict[str, Callable]) -> object: 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: - omni.log.warn(f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default.") + logger.warning( + f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default." + ) # 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: - omni.log.error(f"Unsupported teleop device: {args_cli.teleop_device}") - omni.log.error("Supported devices: keyboard, spacemouse, handtracking") + logger.error(f"Unsupported teleop device: {args_cli.teleop_device}") + logger.error("Supported devices: keyboard, spacemouse, handtracking") exit(1) # Add callbacks to fallback device for key, callback in callbacks.items(): teleop_interface.add_callback(key, callback) except Exception as e: - omni.log.error(f"Failed to create teleop device: {e}") + logger.error(f"Failed to create teleop device: {e}") exit(1) if teleop_interface is None: - omni.log.error("Failed to create teleop interface") + logger.error("Failed to create teleop interface") exit(1) return teleop_interface diff --git a/scripts/tools/replay_demos.py b/scripts/tools/replay_demos.py index c23e3a10d87c..7d5e477267bf 100644 --- a/scripts/tools/replay_demos.py +++ b/scripts/tools/replay_demos.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -32,6 +32,12 @@ " --num_envs is 1." ), ) +parser.add_argument( + "--validate_success_rate", + action="store_true", + default=False, + help="Validate the replay success rate using the task environment termination criteria", +) parser.add_argument( "--enable_pinocchio", action="store_true", @@ -46,7 +52,8 @@ # 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 + # 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 @@ -57,16 +64,17 @@ """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.manager_based.manipulation.pick_place # noqa: F401 import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.parse_cfg import parse_env_cfg @@ -143,6 +151,18 @@ def main(): env_cfg = parse_env_cfg(env_name, device=args_cli.device, 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 = {} @@ -175,11 +195,20 @@ def main(): # simulate environment -- run everything in inference mode episode_names = list(dataset_file_handler.get_episode_names()) replayed_episode_count = 0 + recorded_episode_count = 0 + + # Track current episode indices for each environment + current_episode_indices = [None] * num_envs + + # Track failed demo IDs + failed_demo_ids = [] + 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 @@ -187,16 +216,43 @@ def main(): 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 + if ( + current_episode_indices[env_id] is not None + and current_episode_indices[env_id] not in failed_demo_ids + ): + failed_demo_ids.append(current_episode_indices[env_id]) + + 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 ) @@ -224,7 +280,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) @@ -238,6 +294,16 @@ def main(): # 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_cosmos_prompt_gen.py b/scripts/tools/test/test_cosmos_prompt_gen.py index 1520397d5cbc..17f1764d914b 100644 --- a/scripts/tools/test/test_cosmos_prompt_gen.py +++ b/scripts/tools/test/test_cosmos_prompt_gen.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 1581e0598541..33ccd0d1723e 100644 --- a/scripts/tools/test/test_hdf5_to_mp4.py +++ b/scripts/tools/test/test_hdf5_to_mp4.py @@ -1,15 +1,15 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 h5py +import numpy as np import pytest from scripts.tools.hdf5_to_mp4 import get_num_demos, main, write_demo_to_mp4 @@ -18,7 +18,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 @@ -48,7 +48,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): diff --git a/scripts/tools/test/test_mp4_to_hdf5.py b/scripts/tools/test/test_mp4_to_hdf5.py index 1aa2ee8fc37c..631ac41da228 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 Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/pretrained_checkpoint.py b/scripts/tools/train_and_publish_checkpoints.py similarity index 82% rename from scripts/tools/pretrained_checkpoint.py rename to scripts/tools/train_and_publish_checkpoints.py index a62514eedd97..97ebb6f4c5f7 100644 --- a/scripts/tools/pretrained_checkpoint.py +++ b/scripts/tools/train_and_publish_checkpoints.py @@ -1,10 +1,44 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 manage pretrained checkpoints for our environments. +"""Script to manage pretrained checkpoints for Isaac Lab environments. + +This script is used to train and publish pretrained checkpoints for Isaac Lab environments. +It supports multiple workflows: rl_games, rsl_rl, sb3, and skrl. + +* To train an agent using the rl_games workflow on the Isaac-Cartpole-v0 environment: + + .. code-block:: shell + + python scripts/tools/train_and_publish_checkpoints.py --train rl_games:Isaac-Cartpole-v0 + +* To train and publish the checkpoints for all workflows on only the direct Cartpole environments: + + .. code-block:: shell + + python scripts/tools/train_and_publish_checkpoints.py \ + -tp "*:Isaac-Cartpole-*Direct-v0" \ + --/persistent/isaaclab/asset_root/pretrained_checkpoints="/some/path" + +* To review all repose cube jobs, excluding the 'Play' tasks and 'skrl' workflows: + + .. code-block:: shell + + python scripts/tools/train_and_publish_checkpoints.py \ + -r "*:*Repose-Cube*" \ + --exclude "*:*Play*" \ + --exclude skrl:* + +* To publish all results (that have been reviewed and approved). + + .. code-block:: shell + + python scripts/tools/train_and_publish_checkpoints.py \ + --publish --all \ + --/persistent/isaaclab/asset_root/pretrained_checkpoints="/some/path" + """ import argparse @@ -14,18 +48,21 @@ # Initialize the parser parser = argparse.ArgumentParser( description=""" -Script used for the training and publishing of pre-trained checkpoints for Isaac Lab. +Script for training and publishing pre-trained checkpoints in Isaac Lab. + +Examples: + # Train an agent using the rl_games workflow for the Isaac-Cartpole-v0 environment. + train_and_publish_checkpoints.py --train rl_games:Isaac-Cartpole-v0 -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" \\ + # Train and publish checkpoints for all workflows, targeting only direct Cartpole environments. + train_and_publish_checkpoints.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 \\ + + # Review all Repose Cube jobs, excluding Play tasks and skrl jobs. + train_and_publish_checkpoints.py -r "*:*Repose-Cube*" --exclude "*:*Play*" --exclude skrl:* + + # Publish all results that have been reviewed and approved. + train_and_publish_checkpoints.py --publish --all \\ --/persistent/isaaclab/asset_root/pretrained_checkpoints="/some/path" """, formatter_class=argparse.RawTextHelpFormatter, @@ -36,10 +73,13 @@ "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 +A job consists of a workflow and a task name, separated by a colon (wildcards are optional). Examples: + + rl_games:Isaac-Humanoid-*v0 # Wildcard for any Humanoid version + rsl_rl:Isaac-Ant-*-v0 # Wildcard for any Ant environment + *:Isaac-Velocity-Flat-Spot-v0 # Wildcard for any workflow, specific task + +Wildcards can be used in either the workflow or task name to match multiple entries. """, ) parser.add_argument("-t", "--train", action="store_true", help="Train checkpoints for later publishing.") @@ -84,17 +124,18 @@ # Now everything else import fnmatch -import gymnasium as gym import json -import numpy as np import os import subprocess import sys +import gymnasium as gym +import numpy as np + import omni.client from omni.client._omniclient import CopyBehavior -from isaaclab.utils.pretrained_checkpoint import ( +from isaaclab_rl.utils.pretrained_checkpoint import ( WORKFLOW_EXPERIMENT_NAME_VARIABLE, WORKFLOW_PLAYER, WORKFLOW_TRAINER, @@ -277,7 +318,6 @@ def publish_pretrained_checkpoint(workflow, task_name, force_publish=False): # 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) @@ -315,7 +355,6 @@ def get_job_summary_row(workflow, task_name): def main(): - # Figure out what workflows and tasks we'll be using if args.all: jobs = ["*:*"] @@ -365,7 +404,6 @@ def main(): if __name__ == "__main__": - try: # Run the main function main() diff --git a/scripts/tutorials/00_sim/create_empty.py b/scripts/tutorials/00_sim/create_empty.py index f35a0d5487f0..6fa283a68f1f 100644 --- a/scripts/tutorials/00_sim/create_empty.py +++ b/scripts/tutorials/00_sim/create_empty.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/00_sim/launch_app.py b/scripts/tutorials/00_sim/launch_app.py index 266a1d9a2fe8..1622d3ba956e 100644 --- a/scripts/tutorials/00_sim/launch_app.py +++ b/scripts/tutorials/00_sim/launch_app.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/00_sim/log_time.py b/scripts/tutorials/00_sim/log_time.py index ccec2d579fed..2e4445c3d470 100644 --- a/scripts/tutorials/00_sim/log_time.py +++ b/scripts/tutorials/00_sim/log_time.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/00_sim/set_rendering_mode.py b/scripts/tutorials/00_sim/set_rendering_mode.py index 31d338f742a7..38a1d5b2ba88 100644 --- a/scripts/tutorials/00_sim/set_rendering_mode.py +++ b/scripts/tutorials/00_sim/set_rendering_mode.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -41,7 +41,8 @@ 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 + # 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 diff --git a/scripts/tutorials/00_sim/spawn_prims.py b/scripts/tutorials/00_sim/spawn_prims.py index a544f5a470b2..7c120bd308dd 100644 --- a/scripts/tutorials/00_sim/spawn_prims.py +++ b/scripts/tutorials/00_sim/spawn_prims.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +31,6 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -51,7 +49,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, diff --git a/scripts/tutorials/01_assets/add_new_robot.py b/scripts/tutorials/01_assets/add_new_robot.py index 7755caa6787d..bc322d109479 100644 --- a/scripts/tutorials/01_assets/add_new_robot.py +++ b/scripts/tutorials/01_assets/add_new_robot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/01_assets/run_articulation.py b/scripts/tutorials/01_assets/run_articulation.py index 433825e1a3d0..bc4254cbae1f 100644 --- a/scripts/tutorials/01_assets/run_articulation.py +++ b/scripts/tutorials/01_assets/run_articulation.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -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() diff --git a/scripts/tutorials/01_assets/run_deformable_object.py b/scripts/tutorials/01_assets/run_deformable_object.py index a309a2c6926f..3623bb3d8a19 100644 --- a/scripts/tutorials/01_assets/run_deformable_object.py +++ b/scripts/tutorials/01_assets/run_deformable_object.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -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 DeformableObject, DeformableObjectCfg @@ -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) # Deformable Object cfg = DeformableObjectCfg( diff --git a/scripts/tutorials/01_assets/run_rigid_object.py b/scripts/tutorials/01_assets/run_rigid_object.py index 03ff929f0ec0..dc1a88c57eb1 100644 --- a/scripts/tutorials/01_assets/run_rigid_object.py +++ b/scripts/tutorials/01_assets/run_rigid_object.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -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( diff --git a/scripts/tutorials/01_assets/run_surface_gripper.py b/scripts/tutorials/01_assets/run_surface_gripper.py index 6b8e32d2127a..066dd9a077d2 100644 --- a/scripts/tutorials/01_assets/run_surface_gripper.py +++ b/scripts/tutorials/01_assets/run_surface_gripper.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,8 +35,6 @@ import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, SurfaceGripper, SurfaceGripperCfg from isaaclab.sim import SimulationContext @@ -60,9 +58,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() diff --git a/scripts/tutorials/02_scene/create_scene.py b/scripts/tutorials/02_scene/create_scene.py index 7e819a35f3f5..82b5b21c0097 100644 --- a/scripts/tutorials/02_scene/create_scene.py +++ b/scripts/tutorials/02_scene/create_scene.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/03_envs/create_cartpole_base_env.py b/scripts/tutorials/03_envs/create_cartpole_base_env.py index aa6f2f750ff2..35c3650e6811 100644 --- a/scripts/tutorials/03_envs/create_cartpole_base_env.py +++ b/scripts/tutorials/03_envs/create_cartpole_base_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -36,6 +36,7 @@ """Rest everything follows.""" import math + import torch import isaaclab.envs.mdp as mdp diff --git a/scripts/tutorials/03_envs/create_cube_base_env.py b/scripts/tutorials/03_envs/create_cube_base_env.py index 0d06f370aa60..641512607e31 100644 --- a/scripts/tutorials/03_envs/create_cube_base_env.py +++ b/scripts/tutorials/03_envs/create_cube_base_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -56,11 +56,10 @@ 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 diff --git a/scripts/tutorials/03_envs/create_quadruped_base_env.py b/scripts/tutorials/03_envs/create_quadruped_base_env.py index 3e3b63f76e6b..78f5b75ec5f8 100644 --- a/scripts/tutorials/03_envs/create_quadruped_base_env.py +++ b/scripts/tutorials/03_envs/create_quadruped_base_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/03_envs/policy_inference_in_usd.py b/scripts/tutorials/03_envs/policy_inference_in_usd.py index fcef884d9c95..f4add0f617cf 100644 --- a/scripts/tutorials/03_envs/policy_inference_in_usd.py +++ b/scripts/tutorials/03_envs/policy_inference_in_usd.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -39,6 +39,7 @@ """Rest everything follows.""" import io import os + import torch import omni diff --git a/scripts/tutorials/03_envs/run_cartpole_rl_env.py b/scripts/tutorials/03_envs/run_cartpole_rl_env.py index 3d4d0e53e9c8..eb66a744b958 100644 --- a/scripts/tutorials/03_envs/run_cartpole_rl_env.py +++ b/scripts/tutorials/03_envs/run_cartpole_rl_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/04_sensors/add_sensors_on_robot.py b/scripts/tutorials/04_sensors/add_sensors_on_robot.py index 1d68f72393d6..5cc6de6778b6 100644 --- a/scripts/tutorials/04_sensors/add_sensors_on_robot.py +++ b/scripts/tutorials/04_sensors/add_sensors_on_robot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/04_sensors/run_frame_transformer.py b/scripts/tutorials/04_sensors/run_frame_transformer.py index 33c502ba5f81..d6d12665ada9 100644 --- a/scripts/tutorials/04_sensors/run_frame_transformer.py +++ b/scripts/tutorials/04_sensors/run_frame_transformer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -33,6 +33,7 @@ """Rest everything follows.""" import math + import torch import isaacsim.util.debug_draw._debug_draw as omni_debug_draw diff --git a/scripts/tutorials/04_sensors/run_ray_caster.py b/scripts/tutorials/04_sensors/run_ray_caster.py index 484b523beb5b..51780accbdd2 100644 --- a/scripts/tutorials/04_sensors/run_ray_caster.py +++ b/scripts/tutorials/04_sensors/run_ray_caster.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -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", diff --git a/scripts/tutorials/04_sensors/run_ray_caster_camera.py b/scripts/tutorials/04_sensors/run_ray_caster_camera.py index 77b3f2e6e0bd..375a0cf8f08b 100644 --- a/scripts/tutorials/04_sensors/run_ray_caster_camera.py +++ b/scripts/tutorials/04_sensors/run_ray_caster_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -36,9 +36,9 @@ """Rest everything follows.""" import os + import torch -import isaacsim.core.utils.prims as prim_utils import omni.replicator.core as rep import isaaclab.sim as sim_utils @@ -53,8 +53,8 @@ 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( diff --git a/scripts/tutorials/04_sensors/run_usd_camera.py b/scripts/tutorials/04_sensors/run_usd_camera.py index f9ae93bb0e11..c2462aaaec89 100644 --- a/scripts/tutorials/04_sensors/run_usd_camera.py +++ b/scripts/tutorials/04_sensors/run_usd_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -60,12 +60,12 @@ """Rest everything follows.""" -import numpy as np import os import random + +import numpy as np import torch -import isaacsim.core.utils.prims as prim_utils import omni.replicator.core as rep import isaaclab.sim as sim_utils @@ -82,8 +82,8 @@ 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", update_period=0, @@ -124,7 +124,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 diff --git a/scripts/tutorials/05_controllers/run_diff_ik.py b/scripts/tutorials/05_controllers/run_diff_ik.py index 606a2b8b1c0d..22d17963235f 100644 --- a/scripts/tutorials/05_controllers/run_diff_ik.py +++ b/scripts/tutorials/05_controllers/run_diff_ik.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tutorials/05_controllers/run_osc.py b/scripts/tutorials/05_controllers/run_osc.py index 617945752fad..98b2532a0a2d 100644 --- a/scripts/tutorials/05_controllers/run_osc.py +++ b/scripts/tutorials/05_controllers/run_osc.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/config/extension.toml b/source/isaaclab/config/extension.toml index 818bd291aaf2..9939dcb511b4 100644 --- a/source/isaaclab/config/extension.toml +++ b/source/isaaclab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.47.3" +version = "0.54.2" # Description title = "Isaac Lab framework for Robot Learning" @@ -18,7 +18,8 @@ requirements = [ "toml", "hidapi", "gymnasium==0.29.0", - "trimesh" + "trimesh", + "websockets" ] modules = [ @@ -27,7 +28,8 @@ modules = [ "toml", "hid", "gymnasium", - "trimesh" + "trimesh", + "websockets" ] use_online_index=true diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst index d313eff41e9e..3e7c97f33545 100644 --- a/source/isaaclab/docs/CHANGELOG.rst +++ b/source/isaaclab/docs/CHANGELOG.rst @@ -1,12 +1,540 @@ Changelog --------- -0.47.3 (2025-10-22) +0.54.2 (2026-01-28) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Moved :mod:`isaaclab.sensors.tacsl_sensor` to :mod:`isaaclab_contrib.sensors.tacsl_sensor` module, + since it is not completely ready for release yet. + + +0.54.1 (2026-01-25) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added test suite for ray caster patterns with comprehensive parameterized tests. + +Fixed +^^^^^ + +* Fixed incorrect horizontal angle calculation in :func:`~isaaclab.sensors.ray_caster.patterns.patterns.lidar_pattern` + that caused the actual angular resolution to differ from the requested resolution. + + +0.54.0 (2026-01-13) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added Fabric backend support to :class:`~isaaclab.sim.views.XformPrimView` for GPU-accelerated + batch transform operations on all Boundable prims using Warp kernels. +* Added :mod:`~isaaclab.sim.utils.fabric_utils` module with Warp kernels for efficient Fabric matrix operations. + +Changed +^^^^^^^ + +* Changed :class:`~isaaclab.sensors.camera.Camera` to use Fabric backend for faster pose queries. + + +0.53.2 (2026-01-14) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :class:`~isaaclab.assets.utils.wrench_composer.WrenchComposer` to compose forces and torques at the body's center of mass frame. +* Added :meth:`~isaaclab.assets.Articulation.instantaneous_wrench_composer` to add or set instantaneous external wrenches to the articulation. +* Added :meth:`~isaaclab.assets.Articulation.permanent_wrench_composer` to add or set permanent external wrenches to the articulation. +* Added :meth:`~isaaclab.assets.RigidObject.instantaneous_wrench_composer` to add or set instantaneous external wrenches to the rigid object. +* Added :meth:`~isaaclab.assets.RigidObject.permanent_wrench_composer` to add or set permanent external wrenches to the rigid object. +* Added :meth:`~isaaclab.assets.RigidObjectCollection.instantaneous_wrench_composer` to add or set instantaneous external wrenches to the rigid object collection. +* Added :meth:`~isaaclab.assets.RigidObjectCollection.permanent_wrench_composer` to add or set permanent external wrenches to the rigid object collection. +* Added unit tests for the wrench composer. +* Added kernels for the wrench composer in the :mod:`isaaclab.utils.warp.kernels` module. + +Changed +^^^^^^^ + +* Deprecated :meth:`~isaaclab.assets.Articulation.set_external_force_and_torque` in favor of :meth:`~isaaclab.assets.Articulation.permanent_wrench_composer.set_forces_and_torques`. +* Deprecated :meth:`~isaaclab.assets.RigidObject.set_external_force_and_torque` in favor of :meth:`~isaaclab.assets.RigidObject.permanent_wrench_composer.set_forces_and_torques`. +* Deprecated :meth:`~isaaclab.assets.RigidObjectCollection.set_external_force_and_torque` in favor of :meth:`~isaaclab.assets.RigidObjectCollection.permanent_wrench_composer.set_forces_and_torques`. +* Modified the tests of the articulation, rigid object, and rigid object collection to use the new permanent and instantaneous external wrench functions and test them. + +0.53.1 (2026-01-08) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added function :func:`~isaaclab.sim.utils.prims.change_prim_property` to change attributes on a USD prim. + This replaces the previously used USD command ``ChangeProperty`` that depends on Omniverse Kit API. + +Changed +^^^^^^^ + +* Replaced occurrences of ``ChangeProperty`` USD command to :func:`~isaaclab.sim.utils.prims.change_prim_property`. + + +0.53.0 (2026-01-07) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :class:`~isaaclab.sim.views.XformPrimView` class to provide a + view of the USD Xform operations. Compared to Isaac Sim implementation, + this class optimizes several operations using USD SDF API. + +Changed +^^^^^^^ + +* Switched the sensor classes to use the :class:`~isaaclab.sim.views.XformPrimView` + class for the internal view wherever applicable. + +Removed +^^^^^^^ + +* Removed the usage of :class:`isaacsim.core.utils.prims.XformPrim` + class from the sensor classes. + + +0.52.2 (2026-01-06) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Improved logic for the URDF importer extension version pinning: the older extension version + is now pinned only on Isaac Sim 5.1 and later, while older Isaac Sim versions no longer + attempt to pin to a version that does not exist. + + +0.52.1 (2026-01-02) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed FrameTransformer body name collision when tracking bodies with the same name but different hierarchical paths + (e.g., Robot/left_hand vs Robot_1/left_hand). The sensor now uses the full prim path (with env_* patterns normalized) + as the unique body identifier instead of just the leaf body name. This ensures bodies at different hierarchy levels + are tracked separately. The change is backwards compatible: user-facing frame names still default to leaf names when + not explicitly provided, while internal body tracking uses full paths to avoid collisions. Works for both + environment-scoped paths (e.g., /World/envs/env_0/Robot) and non-environment paths (e.g., /World/Robot). + + +0.52.0 (2026-01-02) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :mod:`~isaaclab.sim.utils.transforms` module to handle USD Xform operations. +* Added passing of ``stage`` to the :func:`~isaaclab.sim.utils.prims.create_prim` function + inside spawning functions to allow for the creation of prims in a specific stage. + +Changed +^^^^^^^ + +* Changed :func:`~isaaclab.sim.utils.prims.create_prim` function to use the :mod:`~isaaclab.sim.utils.transforms` + module for USD Xform operations. It removes the usage of Isaac Sim's XformPrim class. + + +0.51.2 (2025-12-30) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed :attr:`~isaaclab.managers.ObservationManager.get_active_iterable_terms` + to handle observation data when not concatenated along the last dimension. + + +0.51.1 (2025-12-29) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :func:`~isaaclab.utils.version.get_isaac_sim_version` to get the version of Isaac Sim. + This function caches the version of Isaac Sim and returns it immediately if it has already been computed. + This helps avoid parsing the VERSION file from disk multiple times. + +Changed +^^^^^^^ + +* Changed the function :meth:`~isaaclab.utils.version.compare_versions` to use :mod:`packaging.version.Version` module. +* Changed occurrences of :func:`isaacsim.core.version.get_version` to :func:`~isaaclab.utils.version.get_isaac_sim_version`. + +Removed +^^^^^^^ + +* Removed storing of Isaac Sim version inside the environment base classes defined inside + :mod:`isaaclab.envs` module. + + +0.51.0 (2025-12-29) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added tests for the :mod:`isaaclab.sim.utils.prims` module. +* Added tests for the :mod:`isaaclab.sim.utils.stage` module. +* Created :mod:`isaaclab.sim.utils.legacy` sub-module to keep deprecated functions. + +Removed +^^^^^^^ + +* Removed many unused USD prim and stage related operations from the :mod:`isaaclab.sim.utils` module. +* Moved :mod:`isaaclab.sim.utils.nucleus` sub-module to the ``tests/deps/isaacsim`` directory as it + is only being used for Isaac Sim check scripts. + +Changed +^^^^^^^ + +* Changed the organization of the :mod:`isaaclab.sim.utils` module to make it clearer which functions + are related to the stage and which are related to the prims. +* Modified imports of :mod:`~isaaclab.sim.utils.stage` and :mod:`~isaaclab.sim.utils.prims` modules + to only use the :mod:`isaaclab.sim.utils` module. +* Moved ``logger.py`` to the :mod:`isaaclab.utils` module. + + +0.50.7 (2025-12-29) ~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ +* Moved ``pretrained_checkpoint.py`` to the :mod:`isaaclab_rl.utils` module. + + +0.50.6 (2025-12-18) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed issue where :meth:~isaaclab.envs.mdp.observations.body_pose_w` was modifying the original body pose data + when using slice or int for body_ids in the observation config. A clone of the data is now created to avoid modifying + the original data. + + +0.50.5 (2025-12-15) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :class:`~isaaclab.sensors.MultiMeshRayCaster` sensor to support tracking of dynamic meshes for ray-casting. + We keep the previous implementation of :class:`~isaaclab.sensors.RayCaster` for backwards compatibility. +* Added :mod:`isaaclab.utils.mesh` sub-module to perform various Trimesh and USD Mesh related operations. + + +0.50.4 (2025-12-15) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :attr:`~isaaclab.sim.PhysxCfg.enable_external_forces_every_iteration` to enable external forces every position + iteration. This can help improve the accuracy of velocity updates. Consider enabling this flag if the velocities + generated by the simulation are noisy. +* Added warning when :attr:`~isaaclab.sim.PhysxCfg.enable_external_forces_every_iteration` is set to False and + the solver type is TGS. + + +0.50.3 (2025-12-11) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed missing mesh collision approximation attribute when running :class:`~isaaclab.sim.converters.MeshConverter`. + The collision approximation attribute is now properly set on the USD prim when converting meshes with mesh collision + properties. + + +0.50.2 (2025-11-21) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Prevent randomizing mass to zero in :meth:`~isaaclab.envs.mdp.events.randomize_mass_by_scale` to avoid physics errors. + + +0.50.1 (2025-11-25) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed advanced indexing issue in resetting prev action + in :class:`~isaaclab.envs.mdp.actions.JointPositionToLimitsAction` . + + +0.50.0 (2025-12-8) +~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Implemented ability to attach an imu sensor to xform primitives in a usd file. This PR is based on work by '@GiulioRomualdi' + here: #3094 Addressing issue #3088. + + +0.49.3 (2025-12-03) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :class:`G1TriHandUpperBodyMotionControllerGripperRetargeter` and :class:`G1TriHandUpperBodyMotionControllerGripperRetargeterCfg` for retargeting the gripper state from motion controllers. +* Added unit tests for the retargeters. + + +0.49.2 (2025-11-17) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :attr:`~isaaclab.sensors.contact_sensor.ContactSensorCfg.track_friction_forces` to toggle tracking of friction forces between sensor bodies and filtered bodies. +* Added :attr:`~isaaclab.sensors.contact_sensor.ContactSensorData.friction_forces_w` data field for tracking friction forces. + + +0.49.1 (2025-11-26) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed import from ``isaacsim.core.utils.prims`` to ``isaaclab.sim.utils.prims`` across repo to reduce IsaacLab dependencies. + +0.49.0 (2025-11-10) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Updated the URDF Importer version to 2.4.31 to avoid issues with merging joints on the latest URDF importer in Isaac Sim 5.1 + + +0.48.9 (2025-11-21) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Add navigation state API to IsaacLabManagerBasedRLMimicEnv +* Add optional custom recorder config to MimicEnvCfg + + +0.48.8 (2025-10-15) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :attr:`preserve_order` flag to :class:`~isaaclab.envs.mdp.actions.actions_cfg.JointPositionToLimitsActionCfg` + + +0.48.7 (2025-11-25) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed import from ``isaaclab.sim.utils`` to ``isaaclab.sim.utils.stage`` in ``isaaclab.devices.openxr.xr_anchor_utils.py`` + to properly propagate the Isaac Sim stage context. + + + + +0.48.6 (2025-11-18) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added OpenXR motion controller support for the G1 robot locomanipulation environment + ``Isaac-PickPlace-Locomanipulation-G1-Abs-v0``. This enables teleoperation using XR motion controllers + in addition to hand tracking. +* Added :class:`OpenXRDeviceMotionController` for motion controller-based teleoperation with headset anchoring control. +* Added motion controller-specific retargeters: + * :class:`G1TriHandControllerUpperBodyRetargeterCfg` for upper body and hand control using motion controllers. + * :class:`G1LowerBodyStandingControllerRetargeterCfg` for lower body control using motion controllers. + + +0.48.5 (2025-11-14) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed import from ``isaacsim.core.utils.stage`` to ``isaaclab.sim.utils.stage`` to reduce IsaacLab dependencies. + + +0.48.4 (2025-11-14) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Refactored modules related to the actuator configs in order to remediate a circular import necessary to support future + actuator drive model improvements. + + +0.48.3 (2025-11-13) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Moved retargeter and device declaration out of factory and into the devices/retargeters themselves. + + +0.48.2 (2025-11-13) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed from using :meth:`isaacsim.core.utils.torch.set_seed` to :meth:`~isaaclab.utils.seed.configure_seed` + + +0.48.1 (2025-11-10) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :class:`~isaaclab.devices.haply.HaplyDevice` class for SE(3) teleoperation with dual Haply Inverse3 and Versegrip devices, + supporting robot manipulation with haptic feedback. +* Added demo script ``scripts/demos/haply_teleoperation.py`` and documentation guide in + ``docs/source/how-to/haply_teleoperation.rst`` for Haply-based robot teleoperation. + + +0.48.0 (2025-11-03) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Detected contacts are reported with the threshold of 0.0 (instead of 1.0). This increases the sensitivity of contact + detection. + +Fixed +^^^^^ + +* Removed passing the boolean flag to :meth:`isaaclab.sim.schemas.activate_contact_sensors` when activating contact + sensors. This was incorrectly modifying the threshold attribute to 1.0 when contact sensors were activated. + + +0.47.11 (2025-11-03) +~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the bug where effort limits were being overridden in :class:`~isaaclab.actuators.ActuatorBase` when the ``effort_limit`` parameter is set to None. +* Corrected the unit tests for three effort limit scenarios with proper assertions + + +0.47.10 (2025-11-06) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added ``num_rerenders_on_reset`` parameter to ManagerBasedEnvCfg and DirectRLEnvCfg to configure the number + of render steps to perform after reset. This enables more control over DLSS rendering behavior after reset. + +Changed +^^^^^^^ + +* Added deprecation warning for ``rerender_on_reset`` parameter in ManagerBasedEnv and DirectRLEnv. + + +0.47.9 (2025-11-05) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Fixed termination term bookkeeping in :class:`~isaaclab.managers.TerminationManager`: + per-step termination and last-episode termination bookkeeping are now separated. + last-episode dones are now updated once per step from all term outputs, avoiding per-term overwrites + and ensuring Episode_Termination metrics reflect the actual triggering terms. + + +0.47.8 (2025-11-06) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added parameter :attr:`~isaaclab.terrains.TerrainImporterCfg.use_terrain_origins` to allow generated sub terrains with grid origins. + + +0.47.7 (2025-10-31) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed Pink IK controller qpsolver from osqp to daqp. +* Changed Null Space matrix computation in Pink IK's Null Space Posture Task to a faster matrix pseudo inverse computation. + + +0.47.6 (2025-11-01) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed an issue in recurrent policy evaluation in RSL-RL framework where the recurrent state was not reset after an episode termination. + + +0.47.5 (2025-10-30) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Added docstrings notes to clarify the friction coefficient modeling in Isaac Sim 4.5 and 5.0. + + +0.47.4 (2025-10-30) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Enhanced :meth:`~isaaclab.managers.RecorderManager.export_episodes` method to support customizable sequence of demo IDs: + + - Added argument ``demo_ids`` to :meth:`~isaaclab.managers.RecorderManager.export_episodes` to accept a sequence of integers + for custom episode identifiers. + +* Enhanced :meth:`~isaaclab.utils.datasets.HDF5DatasetFileHandler.write_episode` method to support customizable episode identifiers: + + - Added argument ``demo_id`` to :meth:`~isaaclab.utils.datasets.HDF5DatasetFileHandler.write_episode` to accept a custom integer + for episode identifier. + + +0.47.3 (2025-10-22) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + * Fixed the data type conversion in :class:`~isaaclab.sensors.tiled_camera.TiledCamera` to support the correct data type when converting from numpy arrays to warp arrays on the CPU. diff --git a/source/isaaclab/isaaclab/__init__.py b/source/isaaclab/isaaclab/__init__.py index 7bb50a0eab23..73bd3e99d3ab 100644 --- a/source/isaaclab/isaaclab/__init__.py +++ b/source/isaaclab/isaaclab/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/actuators/__init__.py b/source/isaaclab/isaaclab/actuators/__init__.py index 5ccf5d7b082a..db7d36b00a5e 100644 --- a/source/isaaclab/isaaclab/actuators/__init__.py +++ b/source/isaaclab/isaaclab/actuators/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -23,15 +23,14 @@ """ from .actuator_base import ActuatorBase -from .actuator_cfg import ( - ActuatorBaseCfg, - ActuatorNetLSTMCfg, - ActuatorNetMLPCfg, +from .actuator_base_cfg import ActuatorBaseCfg +from .actuator_net import ActuatorNetLSTM, ActuatorNetMLP +from .actuator_net_cfg import ActuatorNetLSTMCfg, ActuatorNetMLPCfg +from .actuator_pd import DCMotor, DelayedPDActuator, IdealPDActuator, ImplicitActuator, RemotizedPDActuator +from .actuator_pd_cfg import ( DCMotorCfg, DelayedPDActuatorCfg, IdealPDActuatorCfg, ImplicitActuatorCfg, RemotizedPDActuatorCfg, ) -from .actuator_net import ActuatorNetLSTM, ActuatorNetMLP -from .actuator_pd import DCMotor, DelayedPDActuator, IdealPDActuator, ImplicitActuator, RemotizedPDActuator diff --git a/source/isaaclab/isaaclab/actuators/actuator_base.py b/source/isaaclab/isaaclab/actuators/actuator_base.py index 16f04b30b690..4489983366d3 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_base.py +++ b/source/isaaclab/isaaclab/actuators/actuator_base.py @@ -1,20 +1,21 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from abc import ABC, abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING, ClassVar +import torch + import isaaclab.utils.string as string_utils from isaaclab.utils.types import ArticulationActions if TYPE_CHECKING: - from .actuator_cfg import ActuatorBaseCfg + from .actuator_base_cfg import ActuatorBaseCfg class ActuatorBase(ABC): @@ -55,13 +56,21 @@ class ActuatorBase(ABC): effort_limit: torch.Tensor """The effort limit for the actuator group. Shape is (num_envs, num_joints). - For implicit actuators, the :attr:`effort_limit` and :attr:`effort_limit_sim` are the same. + This limit is used differently depending on the actuator type: + + - **Explicit actuators**: Used for internal torque clipping within the actuator model + (e.g., motor torque limits in DC motor models). + - **Implicit actuators**: Same as :attr:`effort_limit_sim` (aliased for consistency). """ effort_limit_sim: torch.Tensor """The effort limit for the actuator group in the simulation. Shape is (num_envs, num_joints). For implicit actuators, the :attr:`effort_limit` and :attr:`effort_limit_sim` are the same. + + - **Explicit actuators**: Typically set to a large value (1.0e9) to avoid double-clipping, + since the actuator model already clips efforts using :attr:`effort_limit`. + - **Implicit actuators**: Same as :attr:`effort_limit` (both values are synchronized). """ velocity_limit: torch.Tensor @@ -123,8 +132,11 @@ def __init__( are not specified in the configuration, then their values provided in the constructor are used. .. note:: - The values in the constructor are typically obtained through the USD schemas corresponding - to the joints in the actuator model. + The values in the constructor are typically obtained through the USD values passed from the PhysX API calls + corresponding to the joints in the actuator model; these values serve as default values if the parameters + are not specified in the cfg. + + Args: cfg: The configuration of the actuator model. @@ -196,7 +208,12 @@ def __init__( ) self.velocity_limit = self._parse_joint_parameter(self.cfg.velocity_limit, self.velocity_limit_sim) - self.effort_limit = self._parse_joint_parameter(self.cfg.effort_limit, self.effort_limit_sim) + # Parse effort_limit with special default handling: + # - If cfg.effort_limit is None, use the original USD value (effort_limit parameter from constructor) + # - Otherwise, use effort_limit_sim as the default + # Please refer to the documentation of the effort_limit and effort_limit_sim parameters for more details. + effort_default = effort_limit if self.cfg.effort_limit is None else self.effort_limit_sim + self.effort_limit = self._parse_joint_parameter(self.cfg.effort_limit, effort_default) # create commands buffers for allocation self.computed_effort = torch.zeros(self._num_envs, self.num_joints, device=self._device) diff --git a/source/isaaclab/isaaclab/actuators/actuator_base_cfg.py b/source/isaaclab/isaaclab/actuators/actuator_base_cfg.py new file mode 100644 index 000000000000..e7a26b52aef4 --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/actuator_base_cfg.py @@ -0,0 +1,165 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from isaaclab.utils import configclass + + +@configclass +class ActuatorBaseCfg: + """Configuration for default actuators in an articulation.""" + + class_type: type = MISSING + """The associated actuator class. + + The class should inherit from :class:`isaaclab.actuators.ActuatorBase`. + """ + + joint_names_expr: list[str] = MISSING + """Articulation's joint names that are part of the group. + + Note: + This can be a list of joint names or a list of regex expressions (e.g. ".*"). + """ + + effort_limit: dict[str, float] | float | None = None + """Force/Torque limit of the joints in the group. Defaults to None. + + This limit is used to clip the computed torque sent to the simulation. If None, the + limit is set to the value specified in the USD joint prim. + + .. attention:: + + The :attr:`effort_limit_sim` attribute should be used to set the effort limit for + the simulation physics solver. + + The :attr:`effort_limit` attribute is used for clipping the effort output of the + actuator model **only** in the case of explicit actuators, such as the + :class:`~isaaclab.actuators.IdealPDActuator`. + + .. note:: + + For implicit actuators, the attributes :attr:`effort_limit` and :attr:`effort_limit_sim` + are equivalent. However, we suggest using the :attr:`effort_limit_sim` attribute because + it is more intuitive. + + """ + + velocity_limit: dict[str, float] | float | None = None + """Velocity limit of the joints in the group. Defaults to None. + + This limit is used by the actuator model. If None, the limit is set to the value specified + in the USD joint prim. + + .. attention:: + + The :attr:`velocity_limit_sim` attribute should be used to set the velocity limit for + the simulation physics solver. + + The :attr:`velocity_limit` attribute is used for clipping the effort output of the + actuator model **only** in the case of explicit actuators, such as the + :class:`~isaaclab.actuators.IdealPDActuator`. + + .. note:: + + For implicit actuators, the attribute :attr:`velocity_limit` is not used. This is to stay + backwards compatible with previous versions of the Isaac Lab, where this parameter was + unused since PhysX did not support setting the velocity limit for the joints using the + PhysX Tensor API. + """ + + effort_limit_sim: dict[str, float] | float | None = None + """Effort limit of the joints in the group applied to the simulation physics solver. Defaults to None. + + The effort limit is used to constrain the computed joint efforts in the physics engine. If the + computed effort exceeds this limit, the physics engine will clip the effort to this value. + + Since explicit actuators (e.g. DC motor), compute and clip the effort in the actuator model, this + limit is by default set to a large value to prevent the physics engine from any additional clipping. + However, at times, it may be necessary to set this limit to a smaller value as a safety measure. + + If None, the limit is resolved based on the type of actuator model: + + * For implicit actuators, the limit is set to the value specified in the USD joint prim. + * For explicit actuators, the limit is set to 1.0e9. + + """ + + velocity_limit_sim: dict[str, float] | float | None = None + """Velocity limit of the joints in the group applied to the simulation physics solver. Defaults to None. + + The velocity limit is used to constrain the joint velocities in the physics engine. The joint will only + be able to reach this velocity if the joint's effort limit is sufficiently large. If the joint is moving + faster than this velocity, the physics engine will actually try to brake the joint to reach this velocity. + + If None, the limit is set to the value specified in the USD joint prim for both implicit and explicit actuators. + + .. tip:: + If the velocity limit is too tight, the physics engine may have trouble converging to a solution. + In such cases, we recommend either keeping this value sufficiently large or tuning the stiffness and + damping parameters of the joint to ensure the limits are not violated. + + """ + + stiffness: dict[str, float] | float | None = MISSING + """Stiffness gains (also known as p-gain) of the joints in the group. + + The behavior of the stiffness is different for implicit and explicit actuators. For implicit actuators, + the stiffness gets set into the physics engine directly. For explicit actuators, the stiffness is used + by the actuator model to compute the joint efforts. + + If None, the stiffness is set to the value from the USD joint prim. + """ + + damping: dict[str, float] | float | None = MISSING + """Damping gains (also known as d-gain) of the joints in the group. + + The behavior of the damping is different for implicit and explicit actuators. For implicit actuators, + the damping gets set into the physics engine directly. For explicit actuators, the damping gain is used + by the actuator model to compute the joint efforts. + + If None, the damping is set to the value from the USD joint prim. + """ + + armature: dict[str, float] | float | None = None + """Armature of the joints in the group. Defaults to None. + + The armature is directly added to the corresponding joint-space inertia. It helps improve the + simulation stability by reducing the joint velocities. + + It is a physics engine solver parameter that gets set into the simulation. + + If None, the armature is set to the value from the USD joint prim. + """ + + friction: dict[str, float] | float | None = None + r"""The static friction coefficient of the joints in the group. Defaults to None. + + The joint static friction is a unitless quantity. It relates the magnitude of the spatial force transmitted + from the parent body to the child body to the maximal static friction force that may be applied by the solver + to resist the joint motion. + + Mathematically, this means that: :math:`F_{resist} \leq \mu F_{spatial}`, where :math:`F_{resist}` + is the resisting force applied by the solver and :math:`F_{spatial}` is the spatial force + transmitted from the parent body to the child body. The simulated static friction effect is therefore + similar to static and Coulomb static friction. + + If None, the joint static friction is set to the value from the USD joint prim. + + Note: In Isaac Sim 4.5, this parameter is modeled as a coefficient. In Isaac Sim 5.0 and later, + it is modeled as an effort (torque or force). + """ + + dynamic_friction: dict[str, float] | float | None = None + """The dynamic friction coefficient of the joints in the group. Defaults to None. + + Note: In Isaac Sim 4.5, this parameter is modeled as a coefficient. In Isaac Sim 5.0 and later, + it is modeled as an effort (torque or force). + """ + + viscous_friction: dict[str, float] | float | None = None + """The viscous friction coefficient of the joints in the group. Defaults to None. + """ diff --git a/source/isaaclab/isaaclab/actuators/actuator_cfg.py b/source/isaaclab/isaaclab/actuators/actuator_cfg.py index e5351e4fa632..5cacb8ffa408 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_cfg.py +++ b/source/isaaclab/isaaclab/actuators/actuator_cfg.py @@ -1,285 +1,36 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -from collections.abc import Iterable -from dataclasses import MISSING -from typing import Literal - -from isaaclab.utils import configclass - -from . import actuator_net, actuator_pd -from .actuator_base import ActuatorBase - - -@configclass -class ActuatorBaseCfg: - """Configuration for default actuators in an articulation.""" - - class_type: type[ActuatorBase] = MISSING - """The associated actuator class. - - The class should inherit from :class:`isaaclab.actuators.ActuatorBase`. - """ - - joint_names_expr: list[str] = MISSING - """Articulation's joint names that are part of the group. - - Note: - This can be a list of joint names or a list of regex expressions (e.g. ".*"). - """ - - effort_limit: dict[str, float] | float | None = None - """Force/Torque limit of the joints in the group. Defaults to None. - - This limit is used to clip the computed torque sent to the simulation. If None, the - limit is set to the value specified in the USD joint prim. - - .. attention:: - - The :attr:`effort_limit_sim` attribute should be used to set the effort limit for - the simulation physics solver. - - The :attr:`effort_limit` attribute is used for clipping the effort output of the - actuator model **only** in the case of explicit actuators, such as the - :class:`~isaaclab.actuators.IdealPDActuator`. - - .. note:: - - For implicit actuators, the attributes :attr:`effort_limit` and :attr:`effort_limit_sim` - are equivalent. However, we suggest using the :attr:`effort_limit_sim` attribute because - it is more intuitive. - - """ - - velocity_limit: dict[str, float] | float | None = None - """Velocity limit of the joints in the group. Defaults to None. - - This limit is used by the actuator model. If None, the limit is set to the value specified - in the USD joint prim. - - .. attention:: - - The :attr:`velocity_limit_sim` attribute should be used to set the velocity limit for - the simulation physics solver. - - The :attr:`velocity_limit` attribute is used for clipping the effort output of the - actuator model **only** in the case of explicit actuators, such as the - :class:`~isaaclab.actuators.IdealPDActuator`. - - .. note:: - - For implicit actuators, the attribute :attr:`velocity_limit` is not used. This is to stay - backwards compatible with previous versions of the Isaac Lab, where this parameter was - unused since PhysX did not support setting the velocity limit for the joints using the - PhysX Tensor API. - """ - - effort_limit_sim: dict[str, float] | float | None = None - """Effort limit of the joints in the group applied to the simulation physics solver. Defaults to None. - - The effort limit is used to constrain the computed joint efforts in the physics engine. If the - computed effort exceeds this limit, the physics engine will clip the effort to this value. - - Since explicit actuators (e.g. DC motor), compute and clip the effort in the actuator model, this - limit is by default set to a large value to prevent the physics engine from any additional clipping. - However, at times, it may be necessary to set this limit to a smaller value as a safety measure. - - If None, the limit is resolved based on the type of actuator model: - - * For implicit actuators, the limit is set to the value specified in the USD joint prim. - * For explicit actuators, the limit is set to 1.0e9. - - """ - - velocity_limit_sim: dict[str, float] | float | None = None - """Velocity limit of the joints in the group applied to the simulation physics solver. Defaults to None. - - The velocity limit is used to constrain the joint velocities in the physics engine. The joint will only - be able to reach this velocity if the joint's effort limit is sufficiently large. If the joint is moving - faster than this velocity, the physics engine will actually try to brake the joint to reach this velocity. - - If None, the limit is set to the value specified in the USD joint prim for both implicit and explicit actuators. - - .. tip:: - If the velocity limit is too tight, the physics engine may have trouble converging to a solution. - In such cases, we recommend either keeping this value sufficiently large or tuning the stiffness and - damping parameters of the joint to ensure the limits are not violated. - - """ - - stiffness: dict[str, float] | float | None = MISSING - """Stiffness gains (also known as p-gain) of the joints in the group. - - The behavior of the stiffness is different for implicit and explicit actuators. For implicit actuators, - the stiffness gets set into the physics engine directly. For explicit actuators, the stiffness is used - by the actuator model to compute the joint efforts. - - If None, the stiffness is set to the value from the USD joint prim. - """ - - damping: dict[str, float] | float | None = MISSING - """Damping gains (also known as d-gain) of the joints in the group. - - The behavior of the damping is different for implicit and explicit actuators. For implicit actuators, - the damping gets set into the physics engine directly. For explicit actuators, the damping gain is used - by the actuator model to compute the joint efforts. - - If None, the damping is set to the value from the USD joint prim. - """ - - armature: dict[str, float] | float | None = None - """Armature of the joints in the group. Defaults to None. - - The armature is directly added to the corresponding joint-space inertia. It helps improve the - simulation stability by reducing the joint velocities. - - It is a physics engine solver parameter that gets set into the simulation. - - If None, the armature is set to the value from the USD joint prim. - """ - - friction: dict[str, float] | float | None = None - r"""The static friction coefficient of the joints in the group. Defaults to None. - - The joint static friction is a unitless quantity. It relates the magnitude of the spatial force transmitted - from the parent body to the child body to the maximal static friction force that may be applied by the solver - to resist the joint motion. - - Mathematically, this means that: :math:`F_{resist} \leq \mu F_{spatial}`, where :math:`F_{resist}` - is the resisting force applied by the solver and :math:`F_{spatial}` is the spatial force - transmitted from the parent body to the child body. The simulated static friction effect is therefore - similar to static and Coulomb static friction. - - If None, the joint static friction is set to the value from the USD joint prim. - """ - - dynamic_friction: dict[str, float] | float | None = None - """The dynamic friction coefficient of the joints in the group. Defaults to None. - """ - - viscous_friction: dict[str, float] | float | None = None - """The viscous friction coefficient of the joints in the group. Defaults to None. - """ - - -""" -Implicit Actuator Models. -""" - - -@configclass -class ImplicitActuatorCfg(ActuatorBaseCfg): - """Configuration for an implicit actuator. - - Note: - The PD control is handled implicitly by the simulation. - """ - - class_type: type = actuator_pd.ImplicitActuator - - -""" -Explicit Actuator Models. -""" - - -@configclass -class IdealPDActuatorCfg(ActuatorBaseCfg): - """Configuration for an ideal PD actuator.""" - - class_type: type = actuator_pd.IdealPDActuator - - -@configclass -class DCMotorCfg(IdealPDActuatorCfg): - """Configuration for direct control (DC) motor actuator model.""" - - class_type: type = actuator_pd.DCMotor - - saturation_effort: float = MISSING - """Peak motor force/torque of the electric DC motor (in N-m).""" - - -@configclass -class ActuatorNetLSTMCfg(DCMotorCfg): - """Configuration for LSTM-based actuator model.""" - - class_type: type = actuator_net.ActuatorNetLSTM - # we don't use stiffness and damping for actuator net - stiffness = None - damping = None - - network_file: str = MISSING - """Path to the file containing network weights.""" - - -@configclass -class ActuatorNetMLPCfg(DCMotorCfg): - """Configuration for MLP-based actuator model.""" - - class_type: type = actuator_net.ActuatorNetMLP - # we don't use stiffness and damping for actuator net - stiffness = None - damping = None - - network_file: str = MISSING - """Path to the file containing network weights.""" - - pos_scale: float = MISSING - """Scaling of the joint position errors input to the network.""" - vel_scale: float = MISSING - """Scaling of the joint velocities input to the network.""" - torque_scale: float = MISSING - """Scaling of the joint efforts output from the network.""" - - input_order: Literal["pos_vel", "vel_pos"] = MISSING - """Order of the inputs to the network. - - The order can be one of the following: - - * ``"pos_vel"``: joint position errors followed by joint velocities - * ``"vel_pos"``: joint velocities followed by joint position errors - """ - - input_idx: Iterable[int] = MISSING - """ - Indices of the actuator history buffer passed as inputs to the network. - - The index *0* corresponds to current time-step, while *n* corresponds to n-th - time-step in the past. The allocated history length is `max(input_idx) + 1`. - """ - - -@configclass -class DelayedPDActuatorCfg(IdealPDActuatorCfg): - """Configuration for a delayed PD actuator.""" - - class_type: type = actuator_pd.DelayedPDActuator - - min_delay: int = 0 - """Minimum number of physics time-steps with which the actuator command may be delayed. Defaults to 0.""" - - max_delay: int = 0 - """Maximum number of physics time-steps with which the actuator command may be delayed. Defaults to 0.""" - - -@configclass -class RemotizedPDActuatorCfg(DelayedPDActuatorCfg): - """Configuration for a remotized PD actuator. - - Note: - The torque output limits for this actuator is derived from a linear interpolation of a lookup table - in :attr:`joint_parameter_lookup`. This table describes the relationship between joint angles and - the output torques. - """ - - class_type: type = actuator_pd.RemotizedPDActuator - - joint_parameter_lookup: list[list[float]] = MISSING - """Joint parameter lookup table. Shape is (num_lookup_points, 3). - - This tensor describes the relationship between the joint angle (rad), the transmission ratio (in/out), - and the output torque (N*m). The table is used to interpolate the output torque based on the joint angle. - """ +import sys +import warnings + +from . import actuator_base_cfg, actuator_net_cfg, actuator_pd_cfg + + +def __getattr__(name): + new_module = None + if name in dir(actuator_pd_cfg): + new_module = actuator_pd_cfg + elif name in dir(actuator_net_cfg): + new_module = actuator_net_cfg + elif name in dir(actuator_base_cfg): + new_module = actuator_base_cfg + + if new_module is not None: + warnings.warn( + f"The module actuator_cfg.py is deprecated. Please import {name} directly from the isaaclab.actuators" + f" package, or from its new module {new_module.__name__}.", + DeprecationWarning, + stacklevel=2, + ) + return getattr(new_module, name) + if name in dir(sys.modules[__name__]): + return vars(sys.modules[__name__])[name] + if name == "__path__": + return __file__ + raise ImportError( + f"Failed to import attribute {name} from actuator_cfg.py. Warning: actuator_cfg.py has been " + + "deprecated. Please import actuator config classes directly from the isaaclab.actuators package.", + ) diff --git a/source/isaaclab/isaaclab/actuators/actuator_net.py b/source/isaaclab/isaaclab/actuators/actuator_net.py index 9b4b1641f2c6..2274d1b78db3 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_net.py +++ b/source/isaaclab/isaaclab/actuators/actuator_net.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,17 +14,18 @@ from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + from isaaclab.utils.assets import read_file from isaaclab.utils.types import ArticulationActions from .actuator_pd import DCMotor if TYPE_CHECKING: - from .actuator_cfg import ActuatorNetLSTMCfg, ActuatorNetMLPCfg + from .actuator_net_cfg import ActuatorNetLSTMCfg, ActuatorNetMLPCfg class ActuatorNetLSTM(DCMotor): diff --git a/source/isaaclab/isaaclab/actuators/actuator_net_cfg.py b/source/isaaclab/isaaclab/actuators/actuator_net_cfg.py new file mode 100644 index 000000000000..eecfe8050ab3 --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/actuator_net_cfg.py @@ -0,0 +1,64 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from collections.abc import Iterable +from dataclasses import MISSING +from typing import Literal + +from isaaclab.utils import configclass + +from . import actuator_net +from .actuator_pd_cfg import DCMotorCfg + + +@configclass +class ActuatorNetLSTMCfg(DCMotorCfg): + """Configuration for LSTM-based actuator model.""" + + class_type: type = actuator_net.ActuatorNetLSTM + # we don't use stiffness and damping for actuator net + stiffness = None + damping = None + + network_file: str = MISSING + """Path to the file containing network weights.""" + + +@configclass +class ActuatorNetMLPCfg(DCMotorCfg): + """Configuration for MLP-based actuator model.""" + + class_type: type = actuator_net.ActuatorNetMLP + # we don't use stiffness and damping for actuator net + + stiffness = None + damping = None + + network_file: str = MISSING + """Path to the file containing network weights.""" + + pos_scale: float = MISSING + """Scaling of the joint position errors input to the network.""" + vel_scale: float = MISSING + """Scaling of the joint velocities input to the network.""" + torque_scale: float = MISSING + """Scaling of the joint efforts output from the network.""" + + input_order: Literal["pos_vel", "vel_pos"] = MISSING + """Order of the inputs to the network. + + The order can be one of the following: + + * ``"pos_vel"``: joint position errors followed by joint velocities + * ``"vel_pos"``: joint velocities followed by joint position errors + """ + + input_idx: Iterable[int] = MISSING + """ + Indices of the actuator history buffer passed as inputs to the network. + + The index *0* corresponds to current time-step, while *n* corresponds to n-th + time-step in the past. The allocated history length is `max(input_idx) + 1`. + """ diff --git a/source/isaaclab/isaaclab/actuators/actuator_pd.py b/source/isaaclab/isaaclab/actuators/actuator_pd.py index 162005dfd176..ff014fa7a58e 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_pd.py +++ b/source/isaaclab/isaaclab/actuators/actuator_pd.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch from isaaclab.utils import DelayBuffer, LinearInterpolation from isaaclab.utils.types import ArticulationActions @@ -17,7 +17,7 @@ from .actuator_base import ActuatorBase if TYPE_CHECKING: - from .actuator_cfg import ( + from .actuator_pd_cfg import ( DCMotorCfg, DelayedPDActuatorCfg, IdealPDActuatorCfg, @@ -25,6 +25,8 @@ RemotizedPDActuatorCfg, ) +# import logger +logger = logging.getLogger(__name__) """ Implicit Actuator Models. @@ -57,7 +59,7 @@ def __init__(self, cfg: ImplicitActuatorCfg, *args, **kwargs): # effort limits if cfg.effort_limit_sim is None and cfg.effort_limit is not None: # throw a warning that we have a replacement for the deprecated parameter - omni.log.warn( + logger.warning( "The object has a value for 'effort_limit'." " This parameter will be removed in the future." " To set the effort limit, please use 'effort_limit_sim' instead." @@ -79,7 +81,7 @@ def __init__(self, cfg: ImplicitActuatorCfg, *args, **kwargs): if cfg.velocity_limit_sim is None and cfg.velocity_limit is not None: # throw a warning that previously this was not set # it leads to different simulation behavior so we want to remain backwards compatible - omni.log.warn( + logger.warning( "The object has a value for 'velocity_limit'." " Previously, although this value was specified, it was not getting used by implicit" " actuators. Since this parameter affects the simulation behavior, we continue to not" @@ -211,16 +213,17 @@ class DCMotor(IdealPDActuator): A DC motor characteristics are defined by the following parameters: - * No-load speed (:math:`\dot{q}_{motor, max}`) : The maximum-rated speed of the motor at 0 Torque (:attr:`velocity_limit`). - * Stall torque (:math:`\tau_{motor, stall}`): The maximum-rated torque produced at 0 speed (:attr:`saturation_effort`). - * Continuous torque (:math:`\tau_{motor, con}`): The maximum torque that can be outputted for a short period. This - is often enforced on the current drives for a DC motor to limit overheating, prevent mechanical damage, or - enforced by electrical limitations.(:attr:`effort_limit`). + * No-load speed (:math:`\dot{q}_{motor, max}`) : The maximum-rated speed of the motor at + zero torque (:attr:`velocity_limit`). + * Stall torque (:math:`\tau_{motor, stall}`): The maximum-rated torque produced at + zero speed (:attr:`saturation_effort`). + * Continuous torque (:math:`\tau_{motor, con}`): The maximum torque that can be outputted for a short period. + This is often enforced on the current drives for a DC motor to limit overheating, prevent mechanical damage, + or enforced by electrical limitations (:attr:`effort_limit`). * Corner velocity (:math:`V_{c}`): The velocity where the torque-speed curve intersects with continuous torque. - Based on these parameters, the instantaneous minimum and maximum torques for velocities between corner velocities - (where torque-speed curve intersects with continuous torque) are defined as follows: - Based on these parameters, the instantaneous minimum and maximum torques for velocities are defined as follows: + Based on these parameters, the instantaneous minimum and maximum torques for velocities between corner velocities + (where torque-speed curve intersects with continuous torque) are defined as follows: .. math:: diff --git a/source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py b/source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py new file mode 100644 index 000000000000..d1f5a6282ad8 --- /dev/null +++ b/source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py @@ -0,0 +1,81 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from isaaclab.utils import configclass + +from . import actuator_pd +from .actuator_base_cfg import ActuatorBaseCfg + +""" +Implicit Actuator Models. +""" + + +@configclass +class ImplicitActuatorCfg(ActuatorBaseCfg): + """Configuration for an implicit actuator. + + Note: + The PD control is handled implicitly by the simulation. + """ + + class_type: type = actuator_pd.ImplicitActuator + + +""" +Explicit Actuator Models. +""" + + +@configclass +class IdealPDActuatorCfg(ActuatorBaseCfg): + """Configuration for an ideal PD actuator.""" + + class_type: type = actuator_pd.IdealPDActuator + + +@configclass +class DCMotorCfg(IdealPDActuatorCfg): + """Configuration for direct control (DC) motor actuator model.""" + + class_type: type = actuator_pd.DCMotor + + saturation_effort: float = MISSING + """Peak motor force/torque of the electric DC motor (in N-m).""" + + +@configclass +class DelayedPDActuatorCfg(IdealPDActuatorCfg): + """Configuration for a delayed PD actuator.""" + + class_type: type = actuator_pd.DelayedPDActuator + + min_delay: int = 0 + """Minimum number of physics time-steps with which the actuator command may be delayed. Defaults to 0.""" + + max_delay: int = 0 + """Maximum number of physics time-steps with which the actuator command may be delayed. Defaults to 0.""" + + +@configclass +class RemotizedPDActuatorCfg(DelayedPDActuatorCfg): + """Configuration for a remotized PD actuator. + + Note: + The torque output limits for this actuator is derived from a linear interpolation of a lookup table + in :attr:`joint_parameter_lookup`. This table describes the relationship between joint angles and + the output torques. + """ + + class_type: type = actuator_pd.RemotizedPDActuator + + joint_parameter_lookup: list[list[float]] = MISSING + """Joint parameter lookup table. Shape is (num_lookup_points, 3). + + This tensor describes the relationship between the joint angle (rad), the transmission ratio (in/out), + and the output torque (N*m). The table is used to interpolate the output torque based on the joint angle. + """ diff --git a/source/isaaclab/isaaclab/app/__init__.py b/source/isaaclab/isaaclab/app/__init__.py index 9e9b53b2f373..b81b6e3c9e5e 100644 --- a/source/isaaclab/isaaclab/app/__init__.py +++ b/source/isaaclab/isaaclab/app/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/app/app_launcher.py b/source/isaaclab/isaaclab/app/app_launcher.py index 1ecfbdd8642a..e986d4b664a0 100644 --- a/source/isaaclab/isaaclab/app/app_launcher.py +++ b/source/isaaclab/isaaclab/app/app_launcher.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,6 +14,7 @@ import argparse import contextlib +import logging import os import re import signal @@ -25,6 +26,9 @@ from isaacsim import SimulationApp +# import logger +logger = logging.getLogger(__name__) + class ExplicitAction(argparse.Action): """Custom action to track if an argument was explicitly passed by the user.""" @@ -194,8 +198,8 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: Valid options are: - ``0``: Disabled - - ``1``: `WebRTC `_ over public network - - ``2``: `WebRTC `_ over local/private network + - ``1``: `WebRTC`_ over public network + - ``2``: `WebRTC`_ over local/private network * ``enable_cameras`` (bool): If True, the app will enable camera sensors and render them, even when in headless mode. This flag must be set to True if the environments contains any camera sensors. @@ -213,15 +217,22 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: If provided as an empty string, the experience file is determined based on the command-line flags: - * If headless and enable_cameras are True, the experience file is set to ``isaaclab.python.headless.rendering.kit``. - * If headless is False and enable_cameras is True, the experience file is set to ``isaaclab.python.rendering.kit``. - * If headless and enable_cameras are False, the experience file is set to ``isaaclab.python.kit``. - * If headless is True and enable_cameras is False, the experience file is set to ``isaaclab.python.headless.kit``. + * If headless and enable_cameras are True, the experience file is set to + ``isaaclab.python.headless.rendering.kit``. + * If headless is False and enable_cameras is True, the experience file is set to + ``isaaclab.python.rendering.kit``. + * If headless and enable_cameras are False, the experience file is set to + ``isaaclab.python.kit``. + * If headless is True and enable_cameras is False, the experience file is set to + ``isaaclab.python.headless.kit``. * ``kit_args`` (str): Optional command line arguments to be passed to Omniverse Kit directly. Arguments should be combined into a single string separated by space. Example usage: --kit_args "--ext-folder=/path/to/ext1 --ext-folder=/path/to/ext2" + + .. _`WebRTC`: https://docs.isaacsim.omniverse.nvidia.com/latest/installation/manual_livestream_clients.html#isaac-sim-short-webrtc-streaming-client + Args: parser: An argument parser instance to be extended with the AppLauncher specific options. """ @@ -529,7 +540,8 @@ def _resolve_livestream_settings(self, launcher_args: dict) -> tuple[int, int]: # Set public IP address of a remote instance public_ip_env = os.environ.get("PUBLIC_IP", "127.0.0.1") - # Process livestream here before launching kit because some of the extensions only work when launched with the kit file + # Process livestream here before launching kit because some of the extensions only work + # when launched with the kit file self._livestream_args = [] if self._livestream >= 1: # Note: Only one livestream extension can be enabled at a time @@ -626,7 +638,8 @@ def _resolve_viewport_settings(self, launcher_args: dict): """Resolve viewport related settings.""" # Check if we can disable the viewport to improve performance # This should only happen if we are running headless and do not require livestreaming or video recording - # This is different from offscreen_render because this only affects the default viewport and not other renderproducts in the scene + # This is different from offscreen_render because this only affects the default viewport and + # not other render-products in the scene self._render_viewport = True if self._headless and not self._livestream and not launcher_args.get("video", False): self._render_viewport = False @@ -791,7 +804,7 @@ def _create_app(self): sys.stdout = open(os.devnull, "w") # noqa: SIM115 # pytest may have left some things in sys.argv, this will check for some of those - # do a mark and sweep to remove any -m pytest and -m isaacsim_ci and -c **/pytest.ini + # do a mark and sweep to remove any -m pytest and -m isaacsim_ci and -c **/pyproject.toml indexes_to_remove = [] for idx, arg in enumerate(sys.argv[:-1]): if arg == "-m": @@ -799,9 +812,8 @@ def _create_app(self): if "pytest" in value_for_dash_m or "isaacsim_ci" in value_for_dash_m: indexes_to_remove.append(idx) indexes_to_remove.append(idx + 1) - if arg == "-c" and "pytest.ini" in sys.argv[idx + 1]: + if arg.startswith("--config-file=") and "pyproject.toml" in arg: indexes_to_remove.append(idx) - indexes_to_remove.append(idx + 1) if arg == "--capture=no": indexes_to_remove.append(idx) for idx in sorted(indexes_to_remove, reverse=True): @@ -828,7 +840,8 @@ def _create_app(self): def _rendering_enabled(self) -> bool: """Check if rendering is required by the app.""" # Indicates whether rendering is required by the app. - # Extensions required for rendering bring startup and simulation costs, so we do not enable them if not required. + # Extensions required for rendering bring startup and simulation costs, so we do not + # enable them if not required. return not self._headless or self._livestream >= 1 or self._enable_cameras or self._xr def _load_extensions(self): @@ -883,7 +896,6 @@ def _hide_stop_button(self): def _set_rendering_mode_settings(self, launcher_args: dict) -> None: """Store RTX rendering mode in carb settings.""" import carb - from isaacsim.core.utils.carb import set_carb_setting rendering_mode = launcher_args.get("rendering_mode") @@ -895,12 +907,11 @@ def _set_rendering_mode_settings(self, launcher_args: dict) -> None: # store rendering mode in carb settings carb_settings = carb.settings.get_settings() - set_carb_setting(carb_settings, "/isaaclab/rendering/rendering_mode", rendering_mode) + carb_settings.set_string("/isaaclab/rendering/rendering_mode", rendering_mode) def _set_animation_recording_settings(self, launcher_args: dict) -> None: """Store animation recording settings in carb settings.""" import carb - from isaacsim.core.utils.carb import set_carb_setting # check if recording is enabled recording_enabled = launcher_args.get("anim_recording_enabled", False) @@ -920,9 +931,9 @@ def _set_animation_recording_settings(self, launcher_args: dict) -> None: # store config in carb settings carb_settings = carb.settings.get_settings() - set_carb_setting(carb_settings, "/isaaclab/anim_recording/enabled", recording_enabled) - set_carb_setting(carb_settings, "/isaaclab/anim_recording/start_time", start_time) - set_carb_setting(carb_settings, "/isaaclab/anim_recording/stop_time", stop_time) + carb_settings.set_bool("/isaaclab/anim_recording/enabled", recording_enabled) + carb_settings.set_float("/isaaclab/anim_recording/start_time", start_time) + carb_settings.set_float("/isaaclab/anim_recording/stop_time", stop_time) def _interrupt_signal_handle_callback(self, signal, frame): """Handle the interrupt signal from the keyboard.""" @@ -995,10 +1006,9 @@ def _start_app_patch(sim_app_instance, *args, **kwargs): def __patch_pxr_gf_matrix4d(self, launcher_args: dict): import traceback - import carb from pxr import Gf - carb.log_warn( + logger.warning( "Due to an issue with Pinocchio and pxr.Gf.Matrix4d, patching the Matrix4d constructor to convert arguments" " into a list of floats." ) @@ -1060,13 +1070,13 @@ def patch_matrix4d(self, *args, **kwargs): original_matrix4d(self, *args, **kwargs) except Exception as e: - carb.log_error(f"Matrix4d wrapper error: {e}") + logger.error(f"Matrix4d wrapper error: {e}") traceback.print_stack() # Fall back to original constructor as last resort try: original_matrix4d(self, *args, **kwargs) except Exception as inner_e: - carb.log_error(f"Original Matrix4d constructor also failed: {inner_e}") + logger.error(f"Original Matrix4d constructor also failed: {inner_e}") # Initialize as identity matrix if all else fails original_matrix4d(self) diff --git a/source/isaaclab/isaaclab/assets/__init__.py b/source/isaaclab/isaaclab/assets/__init__.py index 206e5dd9c5c3..41640e5286f5 100644 --- a/source/isaaclab/isaaclab/assets/__init__.py +++ b/source/isaaclab/isaaclab/assets/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/articulation/__init__.py b/source/isaaclab/isaaclab/assets/articulation/__init__.py index 9429f0fec311..e24b3ba10f47 100644 --- a/source/isaaclab/isaaclab/assets/articulation/__init__.py +++ b/source/isaaclab/isaaclab/assets/articulation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/articulation/articulation.py b/source/isaaclab/isaaclab/assets/articulation/articulation.py index deb8b6479f58..b67ded15ac48 100644 --- a/source/isaaclab/isaaclab/assets/articulation/articulation.py +++ b/source/isaaclab/isaaclab/assets/articulation/articulation.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,15 +8,16 @@ from __future__ import annotations -import torch +import logging from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING -import omni.log +import torch +import warp as wp +from prettytable import PrettyTable + import omni.physics.tensors.impl.api as physx from isaacsim.core.simulation_manager import SimulationManager -from isaacsim.core.version import get_version from pxr import PhysxSchema, UsdPhysics import isaaclab.sim as sim_utils @@ -24,6 +25,8 @@ import isaaclab.utils.string as string_utils from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator from isaaclab.utils.types import ArticulationActions +from isaaclab.utils.version import get_isaac_sim_version +from isaaclab.utils.wrench_composer import WrenchComposer from ..asset_base import AssetBase from .articulation_data import ArticulationData @@ -31,6 +34,9 @@ if TYPE_CHECKING: from .articulation_cfg import ArticulationCfg +# import logger +logger = logging.getLogger(__name__) + class Articulation(AssetBase): """An articulation asset class. @@ -165,6 +171,35 @@ def root_physx_view(self) -> physx.ArticulationView: """ return self._root_physx_view + @property + def instantaneous_wrench_composer(self) -> WrenchComposer: + """Instantaneous wrench composer. + + Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench + composer are only valid for the current simulation step. At the end of the simulation step, the wrenches set + to this object are discarded. This is useful to apply forces that change all the time, things like drag forces + for instance. + + Note: + Permanent wrenches are composed into the instantaneous wrench before the instantaneous wrenches are + applied to the simulation. + """ + return self._instantaneous_wrench_composer + + @property + def permanent_wrench_composer(self) -> WrenchComposer: + """Permanent wrench composer. + + Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench + composer are persistent and are applied to the simulation at every step. This is useful to apply forces that + are constant over a period of time, things like the thrust of a motor for instance. + + Note: + Permanent wrenches are composed into the instantaneous wrench before the instantaneous wrenches are + applied to the simulation. + """ + return self._permanent_wrench_composer + """ Operations. """ @@ -176,10 +211,9 @@ def reset(self, env_ids: Sequence[int] | None = None): # reset actuators for actuator in self.actuators.values(): actuator.reset(env_ids) - # reset external wrench - self._external_force_b[env_ids] = 0.0 - self._external_torque_b[env_ids] = 0.0 - self._external_wrench_positions_b[env_ids] = 0.0 + # reset external wrenches. + self._instantaneous_wrench_composer.reset(env_ids) + self._permanent_wrench_composer.reset(env_ids) def write_data_to_sim(self): """Write external wrenches and joint commands to the simulation. @@ -192,23 +226,33 @@ def write_data_to_sim(self): This ensures that the external wrench is applied at every simulation step. """ # write external wrench - if self.has_external_wrench: - if self.uses_external_wrench_positions: + if self._instantaneous_wrench_composer.active or self._permanent_wrench_composer.active: + if self._instantaneous_wrench_composer.active: + # Compose instantaneous wrench with permanent wrench + self._instantaneous_wrench_composer.add_forces_and_torques( + forces=self._permanent_wrench_composer.composed_force, + torques=self._permanent_wrench_composer.composed_torque, + body_ids=self._ALL_BODY_INDICES_WP, + env_ids=self._ALL_INDICES_WP, + ) + # Apply both instantaneous and permanent wrench to the simulation self.root_physx_view.apply_forces_and_torques_at_position( - force_data=self._external_force_b.view(-1, 3), - torque_data=self._external_torque_b.view(-1, 3), - position_data=self._external_wrench_positions_b.view(-1, 3), + force_data=self._instantaneous_wrench_composer.composed_force_as_torch.view(-1, 3), + torque_data=self._instantaneous_wrench_composer.composed_torque_as_torch.view(-1, 3), + position_data=None, indices=self._ALL_INDICES, - is_global=self._use_global_wrench_frame, + is_global=False, ) else: + # Apply permanent wrench to the simulation self.root_physx_view.apply_forces_and_torques_at_position( - force_data=self._external_force_b.view(-1, 3), - torque_data=self._external_torque_b.view(-1, 3), + force_data=self._permanent_wrench_composer.composed_force_as_torch.view(-1, 3), + torque_data=self._permanent_wrench_composer.composed_torque_as_torch.view(-1, 3), position_data=None, indices=self._ALL_INDICES, - is_global=self._use_global_wrench_frame, + is_global=False, ) + self._instantaneous_wrench_composer.reset() # apply actuator models self._apply_actuator_model() @@ -706,10 +750,10 @@ def write_joint_position_limit_to_sim( ) if warn_limit_violation: # warn level will show in console - omni.log.warn(violation_message) + logger.warning(violation_message) else: # info level is only written to log file - omni.log.info(violation_message) + logger.info(violation_message) # set into simulation self.root_physx_view.set_dof_limits(self._data.joint_pos_limits.cpu(), indices=physx_env_ids.cpu()) @@ -878,7 +922,7 @@ def write_joint_friction_coefficient_to_sim( physx_envs_ids_cpu = physx_env_ids.cpu() # set into simulation - if int(get_version()[2]) < 5: + if get_isaac_sim_version().major < 5: self.root_physx_view.set_dof_friction_coefficients( self._data.joint_friction_coeff.cpu(), indices=physx_envs_ids_cpu ) @@ -906,8 +950,8 @@ def write_joint_dynamic_friction_coefficient_to_sim( joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None, ): - if int(get_version()[2]) < 5: - omni.log.warn("Setting joint dynamic friction coefficients are not supported in Isaac Sim < 5.0") + if get_isaac_sim_version().major < 5: + logger.warning("Setting joint dynamic friction coefficients are not supported in Isaac Sim < 5.0") return # resolve indices physx_env_ids = env_ids @@ -932,8 +976,8 @@ def write_joint_viscous_friction_coefficient_to_sim( joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None, ): - if int(get_version()[2]) < 5: - omni.log.warn("Setting joint viscous friction coefficients are not supported in Isaac Sim < 5.0") + if get_isaac_sim_version().major < 5: + logger.warning("Setting joint viscous friction coefficients are not supported in Isaac Sim < 5.0") return # resolve indices physx_env_ids = env_ids @@ -981,18 +1025,6 @@ def set_external_force_and_torque( # example of disabling external wrench asset.set_external_force_and_torque(forces=torch.zeros(0, 3), torques=torch.zeros(0, 3)) - .. caution:: - If the function is called consecutively with and with different values for ``is_global``, then the - all the external wrenches will be applied in the frame specified by the last call. - - .. code-block:: python - - # example of setting external wrench in the global frame - asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[0], is_global=True) - # example of setting external wrench in the link frame - asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[1], is_global=False) - # Both environments will have the external wrenches applied in the link frame - .. note:: This function does not apply the external wrench to the simulation. It only fills the buffers with the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function @@ -1007,52 +1039,42 @@ def set_external_force_and_torque( is_global: Whether to apply the external wrench in the global frame. Defaults to False. If set to False, the external wrench is applied in the link frame of the articulations' bodies. """ - if forces.any() or torques.any(): - self.has_external_wrench = True - else: - self.has_external_wrench = False + logger.warning( + "The function 'set_external_force_and_torque' will be deprecated in a future release. Please" + " use 'permanent_wrench_composer.set_forces_and_torques' instead." + ) + if forces is None and torques is None: + logger.warning("No forces or torques provided. No permanent external wrench will be applied.") # resolve all indices # -- env_ids if env_ids is None: - env_ids = self._ALL_INDICES + env_ids = self._ALL_INDICES_WP elif not isinstance(env_ids, torch.Tensor): - env_ids = torch.tensor(env_ids, dtype=torch.long, device=self.device) + env_ids = wp.array(env_ids, dtype=wp.int32, device=self.device) + else: + env_ids = wp.from_torch(env_ids.to(torch.int32), dtype=wp.int32) # -- body_ids if body_ids is None: - body_ids = torch.arange(self.num_bodies, dtype=torch.long, device=self.device) + body_ids = self._ALL_BODY_INDICES_WP elif isinstance(body_ids, slice): - body_ids = torch.arange(self.num_bodies, dtype=torch.long, device=self.device)[body_ids] - elif not isinstance(body_ids, torch.Tensor): - body_ids = torch.tensor(body_ids, dtype=torch.long, device=self.device) - - # note: we need to do this complicated indexing since torch doesn't support multi-indexing - # create global body indices from env_ids and env_body_ids - # (env_id * total_bodies_per_env) + body_id - indices = body_ids.repeat(len(env_ids), 1) + env_ids.unsqueeze(1) * self.num_bodies - indices = indices.view(-1) - # set into internal buffers - # note: these are applied in the write_to_sim function - self._external_force_b.flatten(0, 1)[indices] = forces.flatten(0, 1) - self._external_torque_b.flatten(0, 1)[indices] = torques.flatten(0, 1) - - if is_global != self._use_global_wrench_frame: - omni.log.warn( - f"The external wrench frame has been changed from {self._use_global_wrench_frame} to {is_global}. This" - " may lead to unexpected behavior." + body_ids = wp.from_torch( + torch.arange(self.num_bodies, dtype=torch.int32, device=self.device)[body_ids], dtype=wp.int32 ) - self._use_global_wrench_frame = is_global - - # If the positions are not provided, the behavior and performance of the simulation should not be affected. - if positions is not None: - # Generates a flag that is set for a full simulation step. This is done to avoid discarding - # the external wrench positions when multiple calls to this functions are made with and without positions. - self.uses_external_wrench_positions = True - self._external_wrench_positions_b.flatten(0, 1)[indices] = positions.flatten(0, 1) + elif not isinstance(body_ids, torch.Tensor): + body_ids = wp.array(body_ids, dtype=wp.int32, device=self.device) else: - # If the positions are not provided, and the flag is set, then we need to ensure that the desired positions are zeroed. - if self.uses_external_wrench_positions: - self._external_wrench_positions_b.flatten(0, 1)[indices] = 0.0 + body_ids = wp.from_torch(body_ids.to(torch.int32), dtype=wp.int32) + + # Write to wrench composer + self._permanent_wrench_composer.set_forces_and_torques( + forces=wp.from_torch(forces, dtype=wp.vec3f) if forces is not None else None, + torques=wp.from_torch(torques, dtype=wp.vec3f) if torques is not None else None, + positions=wp.from_torch(positions, dtype=wp.vec3f) if positions is not None else None, + body_ids=body_ids, + env_ids=env_ids, + is_global=is_global, + ) def set_joint_position_target( self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None @@ -1139,7 +1161,8 @@ def set_fixed_tendon_stiffness( """Set fixed tendon stiffness into internal buffers. This function does not apply the tendon stiffness to the simulation. It only fills the buffers with - the desired values. To apply the tendon stiffness, call the :meth:`write_fixed_tendon_properties_to_sim` function. + the desired values. To apply the tendon stiffness, call the + :meth:`write_fixed_tendon_properties_to_sim` method. Args: stiffness: Fixed tendon stiffness. Shape is (len(env_ids), len(fixed_tendon_ids)). @@ -1191,7 +1214,8 @@ def set_fixed_tendon_limit_stiffness( """Set fixed tendon limit stiffness efforts into internal buffers. This function does not apply the tendon limit stiffness to the simulation. It only fills the buffers with - the desired values. To apply the tendon limit stiffness, call the :meth:`write_fixed_tendon_properties_to_sim` function. + the desired values. To apply the tendon limit stiffness, call the + :meth:`write_fixed_tendon_properties_to_sim` method. Args: limit_stiffness: Fixed tendon limit stiffness. Shape is (len(env_ids), len(fixed_tendon_ids)). @@ -1243,7 +1267,8 @@ def set_fixed_tendon_rest_length( """Set fixed tendon rest length efforts into internal buffers. This function does not apply the tendon rest length to the simulation. It only fills the buffers with - the desired values. To apply the tendon rest length, call the :meth:`write_fixed_tendon_properties_to_sim` function. + the desired values. To apply the tendon rest length, call the + :meth:`write_fixed_tendon_properties_to_sim` method. Args: rest_length: Fixed tendon rest length. Shape is (len(env_ids), len(fixed_tendon_ids)). @@ -1324,15 +1349,16 @@ def set_spatial_tendon_stiffness( """Set spatial tendon stiffness into internal buffers. This function does not apply the tendon stiffness to the simulation. It only fills the buffers with - the desired values. To apply the tendon stiffness, call the :meth:`write_spatial_tendon_properties_to_sim` function. + the desired values. To apply the tendon stiffness, call the + :meth:`write_spatial_tendon_properties_to_sim` method. Args: stiffness: Spatial tendon stiffness. Shape is (len(env_ids), len(spatial_tendon_ids)). spatial_tendon_ids: The tendon indices to set the stiffness for. Defaults to None (all spatial tendons). env_ids: The environment indices to set the stiffness for. Defaults to None (all environments). """ - if int(get_version()[2]) < 5: - omni.log.warn( + if get_isaac_sim_version().major < 5: + logger.warning( "Spatial tendons are not supported in Isaac Sim < 5.0. Please update to Isaac Sim 5.0 or later." ) return @@ -1355,15 +1381,17 @@ def set_spatial_tendon_damping( """Set spatial tendon damping into internal buffers. This function does not apply the tendon damping to the simulation. It only fills the buffers with - the desired values. To apply the tendon damping, call the :meth:`write_spatial_tendon_properties_to_sim` function. + the desired values. To apply the tendon damping, call the + :meth:`write_spatial_tendon_properties_to_sim` method. Args: damping: Spatial tendon damping. Shape is (len(env_ids), len(spatial_tendon_ids)). - spatial_tendon_ids: The tendon indices to set the damping for. Defaults to None (all spatial tendons). - env_ids: The environment indices to set the damping for. Defaults to None (all environments). + spatial_tendon_ids: The tendon indices to set the damping for. Defaults to None, + which means all spatial tendons. + env_ids: The environment indices to set the damping for. Defaults to None, which means all environments. """ - if int(get_version()[2]) < 5: - omni.log.warn( + if get_isaac_sim_version().major < 5: + logger.warning( "Spatial tendons are not supported in Isaac Sim < 5.0. Please update to Isaac Sim 5.0 or later." ) return @@ -1386,15 +1414,17 @@ def set_spatial_tendon_limit_stiffness( """Set spatial tendon limit stiffness into internal buffers. This function does not apply the tendon limit stiffness to the simulation. It only fills the buffers with - the desired values. To apply the tendon limit stiffness, call the :meth:`write_spatial_tendon_properties_to_sim` function. + the desired values. To apply the tendon limit stiffness, call the + :meth:`write_spatial_tendon_properties_to_sim` method. Args: limit_stiffness: Spatial tendon limit stiffness. Shape is (len(env_ids), len(spatial_tendon_ids)). - spatial_tendon_ids: The tendon indices to set the limit stiffness for. Defaults to None (all spatial tendons). + spatial_tendon_ids: The tendon indices to set the limit stiffness for. Defaults to None, + which means all spatial tendons. env_ids: The environment indices to set the limit stiffness for. Defaults to None (all environments). """ - if int(get_version()[2]) < 5: - omni.log.warn( + if get_isaac_sim_version().major < 5: + logger.warning( "Spatial tendons are not supported in Isaac Sim < 5.0. Please update to Isaac Sim 5.0 or later." ) return @@ -1417,15 +1447,16 @@ def set_spatial_tendon_offset( """Set spatial tendon offset efforts into internal buffers. This function does not apply the tendon offset to the simulation. It only fills the buffers with - the desired values. To apply the tendon offset, call the :meth:`write_spatial_tendon_properties_to_sim` function. + the desired values. To apply the tendon offset, call the + :meth:`write_spatial_tendon_properties_to_sim` method. Args: offset: Spatial tendon offset. Shape is (len(env_ids), len(spatial_tendon_ids)). spatial_tendon_ids: The tendon indices to set the offset for. Defaults to None (all spatial tendons). env_ids: The environment indices to set the offset for. Defaults to None (all environments). """ - if int(get_version()[2]) < 5: - omni.log.warn( + if get_isaac_sim_version().major < 5: + logger.warning( "Spatial tendons are not supported in Isaac Sim < 5.0. Please update to Isaac Sim 5.0 or later." ) return @@ -1447,8 +1478,10 @@ def write_spatial_tendon_properties_to_sim( """Write spatial tendon properties into the simulation. Args: - spatial_tendon_ids: The spatial tendon indices to set the properties for. Defaults to None (all spatial tendons). - env_ids: The environment indices to set the properties for. Defaults to None (all environments). + spatial_tendon_ids: The spatial tendon indices to set the properties for. Defaults to None, + which means all spatial tendons. + env_ids: The environment indices to set the properties for. Defaults to None, + which means all environments. """ # resolve indices physx_env_ids = env_ids @@ -1517,8 +1550,8 @@ def _initialize_impl(self): if self._root_physx_view._backend is None: raise RuntimeError(f"Failed to create articulation at: {root_prim_path_expr}. Please check PhysX logs.") - if int(get_version()[2]) < 5: - omni.log.warn( + if get_isaac_sim_version().major < 5: + logger.warning( "Spatial tendons are not supported in Isaac Sim < 5.0: patching spatial-tendon getter" " and setter to use dummy value" ) @@ -1527,19 +1560,19 @@ def _initialize_impl(self): self._root_physx_view.get_spatial_tendon_dampings = lambda: torch.empty(0, device=self.device) self._root_physx_view.get_spatial_tendon_limit_stiffnesses = lambda: torch.empty(0, device=self.device) self._root_physx_view.get_spatial_tendon_offsets = lambda: torch.empty(0, device=self.device) - self._root_physx_view.set_spatial_tendon_properties = lambda *args, **kwargs: omni.log.warn( + self._root_physx_view.set_spatial_tendon_properties = lambda *args, **kwargs: logger.warning( "Spatial tendons are not supported in Isaac Sim < 5.0: Calling" " set_spatial_tendon_properties has no effect" ) # log information about the articulation - omni.log.info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") - omni.log.info(f"Is fixed root: {self.is_fixed_base}") - omni.log.info(f"Number of bodies: {self.num_bodies}") - omni.log.info(f"Body names: {self.body_names}") - omni.log.info(f"Number of joints: {self.num_joints}") - omni.log.info(f"Joint names: {self.joint_names}") - omni.log.info(f"Number of fixed tendons: {self.num_fixed_tendons}") + logger.info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") + logger.info(f"Is fixed root: {self.is_fixed_base}") + logger.info(f"Number of bodies: {self.num_bodies}") + logger.info(f"Body names: {self.body_names}") + logger.info(f"Number of joints: {self.num_joints}") + logger.info(f"Joint names: {self.joint_names}") + logger.info(f"Number of fixed tendons: {self.num_fixed_tendons}") # container for data access self._data = ArticulationData(self.root_physx_view, self.device) @@ -1560,14 +1593,13 @@ def _initialize_impl(self): def _create_buffers(self): # constants self._ALL_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + self._ALL_BODY_INDICES = torch.arange(self.num_bodies, dtype=torch.long, device=self.device) + self._ALL_INDICES_WP = wp.from_torch(self._ALL_INDICES.to(torch.int32), dtype=wp.int32) + self._ALL_BODY_INDICES_WP = wp.from_torch(self._ALL_BODY_INDICES.to(torch.int32), dtype=wp.int32) - # external forces and torques - self.has_external_wrench = False - self.uses_external_wrench_positions = False - self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device) - self._external_torque_b = torch.zeros_like(self._external_force_b) - self._external_wrench_positions_b = torch.zeros_like(self._external_force_b) - self._use_global_wrench_frame = False + # external wrench composer + self._instantaneous_wrench_composer = WrenchComposer(self) + self._permanent_wrench_composer = WrenchComposer(self) # asset named data self._data.joint_names = self.joint_names @@ -1579,7 +1611,7 @@ def _create_buffers(self): self._data.default_joint_stiffness = self.root_physx_view.get_dof_stiffnesses().to(self.device).clone() self._data.default_joint_damping = self.root_physx_view.get_dof_dampings().to(self.device).clone() self._data.default_joint_armature = self.root_physx_view.get_dof_armatures().to(self.device).clone() - if int(get_version()[2]) < 5: + if get_isaac_sim_version().major < 5: self._data.default_joint_friction_coeff = ( self.root_physx_view.get_dof_friction_coefficients().to(self.device).clone() ) @@ -1713,12 +1745,12 @@ def _process_actuators_cfg(self): friction=self._data.default_joint_friction_coeff[:, joint_ids], dynamic_friction=self._data.default_joint_dynamic_friction_coeff[:, joint_ids], viscous_friction=self._data.default_joint_viscous_friction_coeff[:, joint_ids], - effort_limit=self._data.joint_effort_limits[:, joint_ids], + effort_limit=self._data.joint_effort_limits[:, joint_ids].clone(), velocity_limit=self._data.joint_vel_limits[:, joint_ids], ) # log information on actuator groups model_type = "implicit" if actuator.is_implicit_model else "explicit" - omni.log.info( + logger.info( f"Actuator collection: {actuator_name} with model '{actuator_cfg.class_type.__name__}'" f" (type: {model_type}) and joint names: {joint_names} [{joint_ids}]." ) @@ -1741,7 +1773,7 @@ def _process_actuators_cfg(self): self.write_joint_velocity_limit_to_sim(actuator.velocity_limit_sim, joint_ids=actuator.joint_indices) self.write_joint_armature_to_sim(actuator.armature, joint_ids=actuator.joint_indices) self.write_joint_friction_coefficient_to_sim(actuator.friction, joint_ids=actuator.joint_indices) - if int(get_version()[2]) >= 5: + if get_isaac_sim_version().major >= 5: self.write_joint_dynamic_friction_coefficient_to_sim( actuator.dynamic_friction, joint_ids=actuator.joint_indices ) @@ -1755,14 +1787,14 @@ def _process_actuators_cfg(self): self._data.default_joint_damping[:, actuator.joint_indices] = actuator.damping self._data.default_joint_armature[:, actuator.joint_indices] = actuator.armature self._data.default_joint_friction_coeff[:, actuator.joint_indices] = actuator.friction - if int(get_version()[2]) >= 5: + if get_isaac_sim_version().major >= 5: self._data.default_joint_dynamic_friction_coeff[:, actuator.joint_indices] = actuator.dynamic_friction self._data.default_joint_viscous_friction_coeff[:, actuator.joint_indices] = actuator.viscous_friction # perform some sanity checks to ensure actuators are prepared correctly total_act_joints = sum(actuator.num_joints for actuator in self.actuators.values()) if total_act_joints != (self.num_joints - self.num_fixed_tendons): - omni.log.warn( + logger.warning( "Not all actuators are configured! Total number of actuated joints not equal to number of" f" joints available: {total_act_joints} != {self.num_joints - self.num_fixed_tendons}." ) @@ -1778,7 +1810,7 @@ def _process_actuators_cfg(self): fmt = [f"{v:.2e}" if isinstance(v, float) else str(v) for v in resolution_detail] t.add_row([actuator_group_str, property_str, *fmt]) group_count += 1 - omni.log.warn(f"\nActuatorCfg-USD Value Discrepancy Resolution (matching values are skipped): \n{t}") + logger.warning(f"\nActuatorCfg-USD Value Discrepancy Resolution (matching values are skipped): \n{t}") def _process_tendons(self): """Process fixed and spatial tendons.""" @@ -1787,7 +1819,6 @@ def _process_tendons(self): self._spatial_tendon_names = list() # parse fixed tendons properties if they exist if self.num_fixed_tendons > 0 or self.num_spatial_tendons > 0: - joint_paths = self.root_physx_view.dof_paths[0] # iterate over all joints to find tendons attached to them @@ -1923,13 +1954,29 @@ def _log_articulation_info(self): Note: We purposefully read the values from the simulator to ensure that the values are configured as expected. """ + + # define custom formatters for large numbers and limit ranges + def format_large_number(_, v: float) -> str: + """Format large numbers using scientific notation.""" + if abs(v) >= 1e3: + return f"{v:.1e}" + else: + return f"{v:.3f}" + + def format_limits(_, v: tuple[float, float]) -> str: + """Format limit ranges using scientific notation.""" + if abs(v[0]) >= 1e3 or abs(v[1]) >= 1e3: + return f"[{v[0]:.1e}, {v[1]:.1e}]" + else: + return f"[{v[0]:.3f}, {v[1]:.3f}]" + # read out all joint parameters from simulation # -- gains stiffnesses = self.root_physx_view.get_dof_stiffnesses()[0].tolist() dampings = self.root_physx_view.get_dof_dampings()[0].tolist() # -- properties armatures = self.root_physx_view.get_dof_armatures()[0].tolist() - if int(get_version()[2]) < 5: + if get_isaac_sim_version().major < 5: static_frictions = self.root_physx_view.get_dof_friction_coefficients()[0].tolist() else: friction_props = self.root_physx_view.get_dof_friction_properties() @@ -1943,66 +1990,42 @@ def _log_articulation_info(self): # create table for term information joint_table = PrettyTable() joint_table.title = f"Simulation Joint Information (Prim path: {self.cfg.prim_path})" - if int(get_version()[2]) < 5: - joint_table.field_names = [ - "Index", - "Name", - "Stiffness", - "Damping", - "Armature", - "Static Friction", - "Position Limits", - "Velocity Limits", - "Effort Limits", - ] + # build field names based on Isaac Sim version + field_names = ["Index", "Name", "Stiffness", "Damping", "Armature"] + if get_isaac_sim_version().major < 5: + field_names.append("Static Friction") else: - joint_table.field_names = [ - "Index", - "Name", - "Stiffness", - "Damping", - "Armature", - "Static Friction", - "Dynamic Friction", - "Viscous Friction", - "Position Limits", - "Velocity Limits", - "Effort Limits", - ] - joint_table.float_format = ".3" - joint_table.custom_format["Position Limits"] = lambda f, v: f"[{v[0]:.3f}, {v[1]:.3f}]" + field_names.extend(["Static Friction", "Dynamic Friction", "Viscous Friction"]) + field_names.extend(["Position Limits", "Velocity Limits", "Effort Limits"]) + joint_table.field_names = field_names + + # apply custom formatters to numeric columns + joint_table.custom_format["Stiffness"] = format_large_number + joint_table.custom_format["Damping"] = format_large_number + joint_table.custom_format["Armature"] = format_large_number + joint_table.custom_format["Static Friction"] = format_large_number + if get_isaac_sim_version().major >= 5: + joint_table.custom_format["Dynamic Friction"] = format_large_number + joint_table.custom_format["Viscous Friction"] = format_large_number + joint_table.custom_format["Position Limits"] = format_limits + joint_table.custom_format["Velocity Limits"] = format_large_number + joint_table.custom_format["Effort Limits"] = format_large_number + # set alignment of table columns joint_table.align["Name"] = "l" # add info on each term for index, name in enumerate(self.joint_names): - if int(get_version()[2]) < 5: - joint_table.add_row([ - index, - name, - stiffnesses[index], - dampings[index], - armatures[index], - static_frictions[index], - position_limits[index], - velocity_limits[index], - effort_limits[index], - ]) + # build row data based on Isaac Sim version + row_data = [index, name, stiffnesses[index], dampings[index], armatures[index]] + if get_isaac_sim_version().major < 5: + row_data.append(static_frictions[index]) else: - joint_table.add_row([ - index, - name, - stiffnesses[index], - dampings[index], - armatures[index], - static_frictions[index], - dynamic_frictions[index], - viscous_frictions[index], - position_limits[index], - velocity_limits[index], - effort_limits[index], - ]) + row_data.extend([static_frictions[index], dynamic_frictions[index], viscous_frictions[index]]) + row_data.extend([position_limits[index], velocity_limits[index], effort_limits[index]]) + # add row to table + joint_table.add_row(row_data) # convert table to string - omni.log.info(f"Simulation parameters for joints in {self.cfg.prim_path}:\n" + joint_table.get_string()) + logger.info(f"Simulation parameters for joints in {self.cfg.prim_path}:\n" + joint_table.get_string()) # read out all fixed tendon parameters from simulation if self.num_fixed_tendons > 0: @@ -2027,20 +2050,30 @@ def _log_articulation_info(self): "Offset", ] tendon_table.float_format = ".3" - joint_table.custom_format["Limits"] = lambda f, v: f"[{v[0]:.3f}, {v[1]:.3f}]" + + # apply custom formatters to tendon table columns + tendon_table.custom_format["Stiffness"] = format_large_number + tendon_table.custom_format["Damping"] = format_large_number + tendon_table.custom_format["Limit Stiffness"] = format_large_number + tendon_table.custom_format["Limits"] = format_limits + tendon_table.custom_format["Rest Length"] = format_large_number + tendon_table.custom_format["Offset"] = format_large_number + # add info on each term for index in range(self.num_fixed_tendons): - tendon_table.add_row([ - index, - ft_stiffnesses[index], - ft_dampings[index], - ft_limit_stiffnesses[index], - ft_limits[index], - ft_rest_lengths[index], - ft_offsets[index], - ]) + tendon_table.add_row( + [ + index, + ft_stiffnesses[index], + ft_dampings[index], + ft_limit_stiffnesses[index], + ft_limits[index], + ft_rest_lengths[index], + ft_offsets[index], + ] + ) # convert table to string - omni.log.info( + logger.info( f"Simulation parameters for fixed tendons in {self.cfg.prim_path}:\n" + tendon_table.get_string() ) @@ -2064,15 +2097,17 @@ def _log_articulation_info(self): tendon_table.float_format = ".3" # add info on each term for index in range(self.num_spatial_tendons): - tendon_table.add_row([ - index, - st_stiffnesses[index], - st_dampings[index], - st_limit_stiffnesses[index], - st_offsets[index], - ]) + tendon_table.add_row( + [ + index, + st_stiffnesses[index], + st_dampings[index], + st_limit_stiffnesses[index], + st_offsets[index], + ] + ) # convert table to string - omni.log.info( + logger.info( f"Simulation parameters for spatial tendons in {self.cfg.prim_path}:\n" + tendon_table.get_string() ) @@ -2091,7 +2126,7 @@ def write_joint_friction_to_sim( .. deprecated:: 2.1.0 Please use :meth:`write_joint_friction_coefficient_to_sim` instead. """ - omni.log.warn( + logger.warning( "The function 'write_joint_friction_to_sim' will be deprecated in a future release. Please" " use 'write_joint_friction_coefficient_to_sim' instead." ) @@ -2109,7 +2144,7 @@ def write_joint_limits_to_sim( .. deprecated:: 2.1.0 Please use :meth:`write_joint_position_limit_to_sim` instead. """ - omni.log.warn( + logger.warning( "The function 'write_joint_limits_to_sim' will be deprecated in a future release. Please" " use 'write_joint_position_limit_to_sim' instead." ) @@ -2128,7 +2163,7 @@ def set_fixed_tendon_limit( .. deprecated:: 2.1.0 Please use :meth:`set_fixed_tendon_position_limit` instead. """ - omni.log.warn( + logger.warning( "The function 'set_fixed_tendon_limit' will be deprecated in a future release. Please" " use 'set_fixed_tendon_position_limit' instead." ) diff --git a/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py b/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py index 99ce46ee6a53..fad16cd8360a 100644 --- a/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py +++ b/source/isaaclab/isaaclab/assets/articulation/articulation_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/articulation/articulation_data.py b/source/isaaclab/isaaclab/assets/articulation/articulation_data.py index 99b2f76abfa2..47902edc0a59 100644 --- a/source/isaaclab/isaaclab/assets/articulation/articulation_data.py +++ b/source/isaaclab/isaaclab/assets/articulation/articulation_data.py @@ -1,18 +1,22 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch +import logging import weakref -import omni.log +import torch + import omni.physics.tensors.impl.api as physx from isaacsim.core.simulation_manager import SimulationManager import isaaclab.utils.math as math_utils from isaaclab.utils.buffers import TimestampedBuffer +# import logger +logger = logging.getLogger(__name__) + class ArticulationData: """Data container for an articulation. @@ -118,7 +122,8 @@ def update(self, dt: float): ## default_root_state: torch.Tensor = None - """Default root state ``[pos, quat, lin_vel, ang_vel]`` in the local environment frame. Shape is (num_instances, 13). + """Default root state ``[pos, quat, lin_vel, ang_vel]`` in the local environment frame. + Shape is (num_instances, 13). The position and quaternion are of the articulation root's actor frame. Meanwhile, the linear and angular velocities are of its center of mass frame. @@ -151,9 +156,10 @@ def update(self, dt: float): default_inertia: torch.Tensor = None """Default inertia for all the bodies in the articulation. Shape is (num_instances, num_bodies, 9). - The inertia tensor should be given with respect to the center of mass, expressed in the articulation links' actor frame. - The values are stored in the order :math:`[I_{xx}, I_{yx}, I_{zx}, I_{xy}, I_{yy}, I_{zy}, I_{xz}, I_{yz}, I_{zz}]`. - However, due to the symmetry of inertia tensors, row- and column-major orders are equivalent. + The inertia tensor should be given with respect to the center of mass, expressed in the articulation links' + actor frame. The values are stored in the order + :math:`[I_{xx}, I_{yx}, I_{zx}, I_{xy}, I_{yy}, I_{zy}, I_{xz}, I_{yz}, I_{zz}]`. However, due to the + symmetry of inertia tensors, row- and column-major orders are equivalent. This quantity is parsed from the USD schema at the time of initialization. """ @@ -196,29 +202,39 @@ def update(self, dt: float): This quantity is configured through the actuator model's :attr:`isaaclab.actuators.ActuatorBaseCfg.friction` parameter. If the parameter's value is None, the value parsed from the USD schema, at the time of initialization, is used. + + Note: + In Isaac Sim 4.5, this parameter is modeled as a coefficient. In Isaac Sim 5.0 and later, + it is modeled as an effort (torque or force). """ default_joint_dynamic_friction_coeff: torch.Tensor = None """Default joint dynamic friction coefficient of all joints. Shape is (num_instances, num_joints). - This quantity is configured through the actuator model's :attr:`isaaclab.actuators.ActuatorBaseCfg.dynamic_friction` - parameter. If the parameter's value is None, the value parsed from the USD schema, at the time of initialization, - is used. + This quantity is configured through the actuator model's + :attr:`isaaclab.actuators.ActuatorBaseCfg.dynamic_friction` parameter. If the parameter's value is None, + the value parsed from the USD schema, at the time of initialization, is used. + + Note: + In Isaac Sim 4.5, this parameter is modeled as a coefficient. In Isaac Sim 5.0 and later, + it is modeled as an effort (torque or force). """ default_joint_viscous_friction_coeff: torch.Tensor = None """Default joint viscous friction coefficient of all joints. Shape is (num_instances, num_joints). - This quantity is configured through the actuator model's :attr:`isaaclab.actuators.ActuatorBaseCfg.viscous_friction` - parameter. If the parameter's value is None, the value parsed from the USD schema, at the time of initialization, - is used. + This quantity is configured through the actuator model's + :attr:`isaaclab.actuators.ActuatorBaseCfg.viscous_friction` parameter. If the parameter's value is None, + the value parsed from the USD schema, at the time of initialization, is used. """ default_joint_pos_limits: torch.Tensor = None """Default joint position limits of all joints. Shape is (num_instances, num_joints, 2). - The limits are in the order :math:`[lower, upper]`. They are parsed from the USD schema at the time of initialization. + The limits are in the order :math:`[lower, upper]`. They are parsed from the USD schema at the + time of initialization. """ + default_fixed_tendon_stiffness: torch.Tensor = None """Default tendon stiffness of all fixed tendons. Shape is (num_instances, num_fixed_tendons). @@ -347,10 +363,18 @@ def update(self, dt: float): """Joint armature provided to the simulation. Shape is (num_instances, num_joints).""" joint_friction_coeff: torch.Tensor = None - """Joint static friction coefficient provided to the simulation. Shape is (num_instances, num_joints).""" + """Joint static friction coefficient provided to the simulation. Shape is (num_instances, num_joints). + + Note: In Isaac Sim 4.5, this parameter is modeled as a coefficient. In Isaac Sim 5.0 and later, + it is modeled as an effort (torque or force). + """ joint_dynamic_friction_coeff: torch.Tensor = None - """Joint dynamic friction coefficient provided to the simulation. Shape is (num_instances, num_joints).""" + """Joint dynamic friction coefficient provided to the simulation. Shape is (num_instances, num_joints). + + Note: In Isaac Sim 4.5, this parameter is modeled as a coefficient. In Isaac Sim 5.0 and later, + it is modeled as an effort (torque or force). + """ joint_viscous_friction_coeff: torch.Tensor = None """Joint viscous friction coefficient provided to the simulation. Shape is (num_instances, num_joints).""" @@ -538,7 +562,8 @@ def root_link_state_w(self): @property def root_com_state_w(self): - """Root center of mass state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. Shape is (num_instances, 13). + """Root center of mass state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. + Shape is (num_instances, 13). The position, quaternion, and linear/angular velocity are of the articulation root link's center of mass frame relative to the world. Center of mass frame is assumed to be the same orientation as the link rather than the @@ -709,8 +734,11 @@ def body_incoming_joint_wrench_b(self) -> torch.Tensor: Shape is (num_instances, num_bodies, 6). All body reaction wrenches are provided including the root body to the world of an articulation. - For more information on joint wrenches, please check the`PhysX documentation `__ - and the underlying `PhysX Tensor API `__ . + For more information on joint wrenches, please check the`PhysX documentation`_ and the underlying + `PhysX Tensor API`_. + + .. _`PhysX documentation`: https://nvidia-omniverse.github.io/PhysX/physx/5.5.1/docs/Articulations.html#link-incoming-joint-force + .. _`PhysX Tensor API`: https://docs.omniverse.nvidia.com/kit/docs/omni_physics/latest/extensions/runtime/source/omni.physics.tensors/docs/api/python.html#omni.physics.tensors.impl.api.ArticulationView.get_link_incoming_joint_force """ if self._body_incoming_joint_wrench_b.timestamp < self._sim_timestamp: @@ -1077,7 +1105,7 @@ def com_quat_b(self) -> torch.Tensor: @property def joint_limits(self) -> torch.Tensor: """Deprecated property. Please use :attr:`joint_pos_limits` instead.""" - omni.log.warn( + logger.warning( "The `joint_limits` property will be deprecated in a future release. Please use `joint_pos_limits` instead." ) return self.joint_pos_limits @@ -1085,7 +1113,7 @@ def joint_limits(self) -> torch.Tensor: @property def default_joint_limits(self) -> torch.Tensor: """Deprecated property. Please use :attr:`default_joint_pos_limits` instead.""" - omni.log.warn( + logger.warning( "The `default_joint_limits` property will be deprecated in a future release. Please use" " `default_joint_pos_limits` instead." ) @@ -1094,7 +1122,7 @@ def default_joint_limits(self) -> torch.Tensor: @property def joint_velocity_limits(self) -> torch.Tensor: """Deprecated property. Please use :attr:`joint_vel_limits` instead.""" - omni.log.warn( + logger.warning( "The `joint_velocity_limits` property will be deprecated in a future release. Please use" " `joint_vel_limits` instead." ) @@ -1103,7 +1131,7 @@ def joint_velocity_limits(self) -> torch.Tensor: @property def joint_friction(self) -> torch.Tensor: """Deprecated property. Please use :attr:`joint_friction_coeff` instead.""" - omni.log.warn( + logger.warning( "The `joint_friction` property will be deprecated in a future release. Please use" " `joint_friction_coeff` instead." ) @@ -1112,7 +1140,7 @@ def joint_friction(self) -> torch.Tensor: @property def default_joint_friction(self) -> torch.Tensor: """Deprecated property. Please use :attr:`default_joint_friction_coeff` instead.""" - omni.log.warn( + logger.warning( "The `default_joint_friction` property will be deprecated in a future release. Please use" " `default_joint_friction_coeff` instead." ) @@ -1121,7 +1149,7 @@ def default_joint_friction(self) -> torch.Tensor: @property def fixed_tendon_limit(self) -> torch.Tensor: """Deprecated property. Please use :attr:`fixed_tendon_pos_limits` instead.""" - omni.log.warn( + logger.warning( "The `fixed_tendon_limit` property will be deprecated in a future release. Please use" " `fixed_tendon_pos_limits` instead." ) @@ -1130,7 +1158,7 @@ def fixed_tendon_limit(self) -> torch.Tensor: @property def default_fixed_tendon_limit(self) -> torch.Tensor: """Deprecated property. Please use :attr:`default_fixed_tendon_pos_limits` instead.""" - omni.log.warn( + logger.warning( "The `default_fixed_tendon_limit` property will be deprecated in a future release. Please use" " `default_fixed_tendon_pos_limits` instead." ) diff --git a/source/isaaclab/isaaclab/assets/asset_base.py b/source/isaaclab/isaaclab/assets/asset_base.py index a618ddc0e133..158b69933510 100644 --- a/source/isaaclab/isaaclab/assets/asset_base.py +++ b/source/isaaclab/isaaclab/assets/asset_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,19 +8,19 @@ import builtins import inspect import re -import torch import weakref from abc import ABC, abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING, Any -import isaacsim.core.utils.prims as prim_utils +import torch + import omni.kit.app import omni.timeline from isaacsim.core.simulation_manager import IsaacEvents, SimulationManager -from isaacsim.core.utils.stage import get_current_stage import isaaclab.sim as sim_utils +from isaaclab.sim.utils.stage import get_current_stage if TYPE_CHECKING: from .asset_base_cfg import AssetBaseCfg @@ -176,7 +176,7 @@ def set_visibility(self, visible: bool, env_ids: Sequence[int] | None = None): # iterate over the environment ids for env_id in env_ids: - prim_utils.set_prim_visibility(self._prims[env_id], visible) + sim_utils.set_prim_visibility(self._prims[env_id], visible) def set_debug_vis(self, debug_vis: bool) -> bool: """Sets whether to visualize the asset data. diff --git a/source/isaaclab/isaaclab/assets/asset_base_cfg.py b/source/isaaclab/isaaclab/assets/asset_base_cfg.py index f02a6f4f765d..a9a8a1d471a8 100644 --- a/source/isaaclab/isaaclab/assets/asset_base_cfg.py +++ b/source/isaaclab/isaaclab/assets/asset_base_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/deformable_object/__init__.py b/source/isaaclab/isaaclab/assets/deformable_object/__init__.py index 88f03cbfe6b7..503a238935bd 100644 --- a/source/isaaclab/isaaclab/assets/deformable_object/__init__.py +++ b/source/isaaclab/isaaclab/assets/deformable_object/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py index 982b2f72c810..12ae6c51ad8f 100644 --- a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py +++ b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py @@ -1,15 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch + import omni.physics.tensors.impl.api as physx from isaacsim.core.simulation_manager import SimulationManager from pxr import PhysxSchema, UsdShade @@ -24,6 +25,9 @@ if TYPE_CHECKING: from .deformable_object_cfg import DeformableObjectCfg +# import logger +logger = logging.getLogger(__name__) + class DeformableObject(AssetBase): """A deformable object asset class. @@ -208,7 +212,8 @@ def write_nodal_kinematic_target_to_sim(self, targets: torch.Tensor, env_ids: Se """Set the kinematic targets of the simulation mesh for the deformable bodies indicated by the indices. The kinematic targets comprise of individual nodal positions of the simulation mesh for the deformable body - and a flag indicating whether the node is kinematically driven or not. The positions are in the simulation frame. + and a flag indicating whether the node is kinematically driven or not. The positions are in the simulation + frame. Note: The flag is set to 0.0 for kinematically driven nodes and 1.0 for free nodes. @@ -307,7 +312,7 @@ def _initialize_impl(self): material_prim = mat_prim break if material_prim is None: - omni.log.info( + logger.info( f"Failed to find a deformable material binding for '{root_prim.GetPath().pathString}'." " The material properties will be set to default values and are not modifiable at runtime." " If you want to modify the material properties, please ensure that the material is bound" @@ -343,14 +348,14 @@ def _initialize_impl(self): self._material_physx_view = None # log information about the deformable body - omni.log.info(f"Deformable body initialized at: {root_prim_path_expr}") - omni.log.info(f"Number of instances: {self.num_instances}") - omni.log.info(f"Number of bodies: {self.num_bodies}") + logger.info(f"Deformable body initialized at: {root_prim_path_expr}") + logger.info(f"Number of instances: {self.num_instances}") + logger.info(f"Number of bodies: {self.num_bodies}") if self._material_physx_view is not None: - omni.log.info(f"Deformable material initialized at: {material_prim_path_expr}") - omni.log.info(f"Number of instances: {self._material_physx_view.count}") + logger.info(f"Deformable material initialized at: {material_prim_path_expr}") + logger.info(f"Number of instances: {self._material_physx_view.count}") else: - omni.log.info("No deformable material found. Material properties will be set to default values.") + logger.info("No deformable material found. Material properties will be set to default values.") # container for data access self._data = DeformableObjectData(self.root_physx_view, self.device) diff --git a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_cfg.py b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_cfg.py index aa36f1829fdd..9ef06d1b54c3 100644 --- a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_cfg.py +++ b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_data.py b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_data.py index 5309c35d386f..bb7714383e06 100644 --- a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_data.py +++ b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object_data.py @@ -1,11 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch import weakref +import torch + import omni.physics.tensors.impl.api as physx import isaaclab.utils.math as math_utils @@ -218,8 +219,8 @@ def collision_element_stress_w(self): @property def root_pos_w(self) -> torch.Tensor: - """Root position from nodal positions of the simulation mesh for the deformable bodies in simulation world frame. - Shape is (num_instances, 3). + """Root position from nodal positions of the simulation mesh for the deformable bodies in simulation + world frame. Shape is (num_instances, 3). This quantity is computed as the mean of the nodal positions. """ diff --git a/source/isaaclab/isaaclab/assets/rigid_object/__init__.py b/source/isaaclab/isaaclab/assets/rigid_object/__init__.py index 94c396776728..1598c060f1a4 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object/__init__.py +++ b/source/isaaclab/isaaclab/assets/rigid_object/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py index 9de2a137636e..6d0ec98f4314 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py +++ b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py @@ -1,15 +1,17 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch +import warp as wp + import omni.physics.tensors.impl.api as physx from isaacsim.core.simulation_manager import SimulationManager from pxr import UsdPhysics @@ -17,6 +19,7 @@ import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils +from isaaclab.utils.wrench_composer import WrenchComposer from ..asset_base import AssetBase from .rigid_object_data import RigidObjectData @@ -24,6 +27,9 @@ if TYPE_CHECKING: from .rigid_object_cfg import RigidObjectCfg +# import logger +logger = logging.getLogger(__name__) + class RigidObject(AssetBase): """A rigid object asset class. @@ -92,6 +98,27 @@ def root_physx_view(self) -> physx.RigidBodyView: """ return self._root_physx_view + @property + def instantaneous_wrench_composer(self) -> WrenchComposer: + """Instantaneous wrench composer. + + Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench + composer are only valid for the current simulation step. At the end of the simulation step, the wrenches set + to this object are discarded. This is useful to apply forces that change all the time, things like drag forces + for instance. + """ + return self._instantaneous_wrench_composer + + @property + def permanent_wrench_composer(self) -> WrenchComposer: + """Permanent wrench composer. + + Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench + composer are persistent and are applied to the simulation at every step. This is useful to apply forces that + are constant over a period of time, things like the thrust of a motor for instance. + """ + return self._permanent_wrench_composer + """ Operations. """ @@ -101,9 +128,8 @@ def reset(self, env_ids: Sequence[int] | None = None): if env_ids is None: env_ids = slice(None) # reset external wrench - self._external_force_b[env_ids] = 0.0 - self._external_torque_b[env_ids] = 0.0 - self._external_wrench_positions_b[env_ids] = 0.0 + self._instantaneous_wrench_composer.reset(env_ids) + self._permanent_wrench_composer.reset(env_ids) def write_data_to_sim(self): """Write external wrench to the simulation. @@ -113,23 +139,33 @@ def write_data_to_sim(self): This ensures that the external wrench is applied at every simulation step. """ # write external wrench - if self.has_external_wrench: - if self.uses_external_wrench_positions: + if self._instantaneous_wrench_composer.active or self._permanent_wrench_composer.active: + if self._instantaneous_wrench_composer.active: + # Compose instantaneous wrench with permanent wrench + self._instantaneous_wrench_composer.add_forces_and_torques( + forces=self._permanent_wrench_composer.composed_force, + torques=self._permanent_wrench_composer.composed_torque, + body_ids=self._ALL_BODY_INDICES_WP, + env_ids=self._ALL_INDICES_WP, + ) + # Apply both instantaneous and permanent wrench to the simulation self.root_physx_view.apply_forces_and_torques_at_position( - force_data=self._external_force_b.view(-1, 3), - torque_data=self._external_torque_b.view(-1, 3), - position_data=self._external_wrench_positions_b.view(-1, 3), + force_data=self._instantaneous_wrench_composer.composed_force_as_torch.view(-1, 3), + torque_data=self._instantaneous_wrench_composer.composed_torque_as_torch.view(-1, 3), + position_data=None, indices=self._ALL_INDICES, - is_global=self._use_global_wrench_frame, + is_global=False, ) else: + # Apply permanent wrench to the simulation self.root_physx_view.apply_forces_and_torques_at_position( - force_data=self._external_force_b.view(-1, 3), - torque_data=self._external_torque_b.view(-1, 3), + force_data=self._permanent_wrench_composer.composed_force_as_torch.view(-1, 3), + torque_data=self._permanent_wrench_composer.composed_torque_as_torch.view(-1, 3), position_data=None, indices=self._ALL_INDICES, - is_global=self._use_global_wrench_frame, + is_global=False, ) + self._instantaneous_wrench_composer.reset() def update(self, dt: float): self._data.update(dt) @@ -388,18 +424,6 @@ def set_external_force_and_torque( # example of disabling external wrench asset.set_external_force_and_torque(forces=torch.zeros(0, 3), torques=torch.zeros(0, 3)) - .. caution:: - If the function is called consecutively with and with different values for ``is_global``, then the - all the external wrenches will be applied in the frame specified by the last call. - - .. code-block:: python - - # example of setting external wrench in the global frame - asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[0], is_global=True) - # example of setting external wrench in the link frame - asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[1], is_global=False) - # Both environments will have the external wrenches applied in the link frame - .. note:: This function does not apply the external wrench to the simulation. It only fills the buffers with the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function @@ -408,46 +432,40 @@ def set_external_force_and_torque( Args: forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). torques: External torques in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). - positions: External wrench positions in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). Defaults to None. + positions: External wrench positions in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). + Defaults to None. body_ids: Body indices to apply external wrench to. Defaults to None (all bodies). env_ids: Environment indices to apply external wrench to. Defaults to None (all instances). is_global: Whether to apply the external wrench in the global frame. Defaults to False. If set to False, the external wrench is applied in the link frame of the bodies. """ - if forces.any() or torques.any(): - self.has_external_wrench = True - else: - self.has_external_wrench = False - # to be safe, explicitly set value to zero - forces = torques = 0.0 + logger.warning( + "The function 'set_external_force_and_torque' will be deprecated in a future release. Please" + " use 'permanent_wrench_composer.set_forces_and_torques' instead." + ) + if forces is None and torques is None: + logger.warning("No forces or torques provided. No permanent external wrench will be applied.") # resolve all indices # -- env_ids if env_ids is None: - env_ids = slice(None) - # -- body_ids - if body_ids is None: - body_ids = slice(None) - # broadcast env_ids if needed to allow double indexing - if env_ids != slice(None) and body_ids != slice(None): - env_ids = env_ids[:, None] - # set into internal buffers - self._external_force_b[env_ids, body_ids] = forces - self._external_torque_b[env_ids, body_ids] = torques - - if is_global != self._use_global_wrench_frame: - omni.log.warn( - f"The external wrench frame has been changed from {self._use_global_wrench_frame} to {is_global}. This" - " may lead to unexpected behavior." - ) - self._use_global_wrench_frame = is_global - - if positions is not None: - self.uses_external_wrench_positions = True - self._external_wrench_positions_b[env_ids, body_ids] = positions + env_ids = self._ALL_INDICES_WP + elif not isinstance(env_ids, torch.Tensor): + env_ids = wp.array(env_ids, dtype=wp.int32, device=self.device) else: - if self.uses_external_wrench_positions: - self._external_wrench_positions_b[env_ids, body_ids] = 0.0 + env_ids = wp.from_torch(env_ids.to(torch.int32), dtype=wp.int32) + # -- body_ids + body_ids = self._ALL_BODY_INDICES_WP + + # Write to wrench composer + self._permanent_wrench_composer.set_forces_and_torques( + forces=wp.from_torch(forces, dtype=wp.vec3f) if forces is not None else None, + torques=wp.from_torch(torques, dtype=wp.vec3f) if torques is not None else None, + positions=wp.from_torch(positions, dtype=wp.vec3f) if positions is not None else None, + body_ids=body_ids, + env_ids=env_ids, + is_global=is_global, + ) """ Internal helper. @@ -505,10 +523,10 @@ def _initialize_impl(self): raise RuntimeError(f"Failed to create rigid body at: {self.cfg.prim_path}. Please check PhysX logs.") # log information about the rigid body - omni.log.info(f"Rigid body initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") - omni.log.info(f"Number of instances: {self.num_instances}") - omni.log.info(f"Number of bodies: {self.num_bodies}") - omni.log.info(f"Body names: {self.body_names}") + logger.info(f"Rigid body initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") + logger.info(f"Number of instances: {self.num_instances}") + logger.info(f"Number of bodies: {self.num_bodies}") + logger.info(f"Body names: {self.body_names}") # container for data access self._data = RigidObjectData(self.root_physx_view, self.device) @@ -524,14 +542,14 @@ def _create_buffers(self): """Create buffers for storing data.""" # constants self._ALL_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + self._ALL_INDICES_WP = wp.from_torch(self._ALL_INDICES.to(torch.int32), dtype=wp.int32) + self._ALL_BODY_INDICES_WP = wp.from_torch( + torch.arange(self.num_bodies, dtype=torch.int32, device=self.device), dtype=wp.int32 + ) - # external forces and torques - self.has_external_wrench = False - self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device) - self._external_torque_b = torch.zeros_like(self._external_force_b) - self.uses_external_wrench_positions = False - self._external_wrench_positions_b = torch.zeros_like(self._external_force_b) - self._use_global_wrench_frame = False + # external wrench composer + self._instantaneous_wrench_composer = WrenchComposer(self) + self._permanent_wrench_composer = WrenchComposer(self) # set information about rigid body into data self._data.body_names = self.body_names diff --git a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_cfg.py b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_cfg.py index 1cd24bcc9183..8340aa45f763 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_cfg.py +++ b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py index ee83900376f6..d1a94f1eac76 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py +++ b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py @@ -1,15 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch import weakref +import torch + import omni.physics.tensors.impl.api as physx import isaaclab.utils.math as math_utils -from isaaclab.sim.utils import get_current_stage_id +from isaaclab.sim.utils.stage import get_current_stage_id from isaaclab.utils.buffers import TimestampedBuffer diff --git a/source/isaaclab/isaaclab/assets/rigid_object_collection/__init__.py b/source/isaaclab/isaaclab/assets/rigid_object_collection/__init__.py index dc1318a1b8da..edca995d62a7 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object_collection/__init__.py +++ b/source/isaaclab/isaaclab/assets/rigid_object_collection/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py index b607f06d0883..b458b15b4035 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py +++ b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py @@ -1,25 +1,26 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +import logging import re -import torch from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.kit.app -import omni.log +import torch +import warp as wp + import omni.physics.tensors.impl.api as physx -import omni.timeline from isaacsim.core.simulation_manager import SimulationManager from pxr import UsdPhysics import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils +from isaaclab.utils.wrench_composer import WrenchComposer from ..asset_base import AssetBase from .rigid_object_collection_data import RigidObjectCollectionData @@ -27,6 +28,9 @@ if TYPE_CHECKING: from .rigid_object_collection_cfg import RigidObjectCollectionCfg +# import logger +logger = logging.getLogger(__name__) + class RigidObjectCollection(AssetBase): """A rigid object collection class. @@ -130,6 +134,27 @@ def root_physx_view(self) -> physx.RigidBodyView: """ return self._root_physx_view # type: ignore + @property + def instantaneous_wrench_composer(self) -> WrenchComposer: + """Instantaneous wrench composer. + + Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench + composer are only valid for the current simulation step. At the end of the simulation step, the wrenches set + to this object are discarded. This is useful to apply forces that change all the time, things like drag forces + for instance. + """ + return self._instantaneous_wrench_composer + + @property + def permanent_wrench_composer(self) -> WrenchComposer: + """Permanent wrench composer. + + Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench + composer are persistent and are applied to the simulation at every step. This is useful to apply forces that + are constant over a period of time, things like the thrust of a motor for instance. + """ + return self._permanent_wrench_composer + """ Operations. """ @@ -147,9 +172,8 @@ def reset(self, env_ids: torch.Tensor | None = None, object_ids: slice | torch.T if object_ids is None: object_ids = self._ALL_OBJ_INDICES # reset external wrench - self._external_force_b[env_ids[:, None], object_ids] = 0.0 - self._external_torque_b[env_ids[:, None], object_ids] = 0.0 - self._external_wrench_positions_b[env_ids[:, None], object_ids] = 0.0 + self._instantaneous_wrench_composer.reset(env_ids) + self._permanent_wrench_composer.reset(env_ids) def write_data_to_sim(self): """Write external wrench to the simulation. @@ -159,23 +183,33 @@ def write_data_to_sim(self): This ensures that the external wrench is applied at every simulation step. """ # write external wrench - if self.has_external_wrench: - if self.uses_external_wrench_positions: + if self._instantaneous_wrench_composer.active or self._permanent_wrench_composer.active: + if self._instantaneous_wrench_composer.active: + # Compose instantaneous wrench with permanent wrench + self._instantaneous_wrench_composer.add_forces_and_torques( + forces=self._permanent_wrench_composer.composed_force, + torques=self._permanent_wrench_composer.composed_torque, + body_ids=self._ALL_OBJ_INDICES_WP, + env_ids=self._ALL_ENV_INDICES_WP, + ) + # Apply both instantaneous and permanent wrench to the simulation self.root_physx_view.apply_forces_and_torques_at_position( - force_data=self.reshape_data_to_view(self._external_force_b), - torque_data=self.reshape_data_to_view(self._external_torque_b), - position_data=self.reshape_data_to_view(self._external_wrench_positions_b), + force_data=self.reshape_data_to_view(self._instantaneous_wrench_composer.composed_force_as_torch), + torque_data=self.reshape_data_to_view(self._instantaneous_wrench_composer.composed_torque_as_torch), + position_data=None, indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES), - is_global=self._use_global_wrench_frame, + is_global=False, ) else: + # Apply permanent wrench to the simulation self.root_physx_view.apply_forces_and_torques_at_position( - force_data=self.reshape_data_to_view(self._external_force_b), - torque_data=self.reshape_data_to_view(self._external_torque_b), + force_data=self.reshape_data_to_view(self._permanent_wrench_composer.composed_force_as_torch), + torque_data=self.reshape_data_to_view(self._permanent_wrench_composer.composed_torque_as_torch), position_data=None, indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES), - is_global=self._use_global_wrench_frame, + is_global=False, ) + self._instantaneous_wrench_composer.reset() def update(self, dt: float): self._data.update(dt) @@ -500,18 +534,6 @@ def set_external_force_and_torque( # example of disabling external wrench asset.set_external_force_and_torque(forces=torch.zeros(0, 0, 3), torques=torch.zeros(0, 0, 3)) - .. caution:: - If the function is called consecutively with and with different values for ``is_global``, then the - all the external wrenches will be applied in the frame specified by the last call. - - .. code-block:: python - - # example of setting external wrench in the global frame - asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[0], is_global=True) - # example of setting external wrench in the link frame - asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[1], is_global=False) - # Both environments will have the external wrenches applied in the link frame - .. note:: This function does not apply the external wrench to the simulation. It only fills the buffers with the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function @@ -526,37 +548,43 @@ def set_external_force_and_torque( is_global: Whether to apply the external wrench in the global frame. Defaults to False. If set to False, the external wrench is applied in the link frame of the bodies. """ - if forces.any() or torques.any(): - self.has_external_wrench = True - else: - self.has_external_wrench = False - # to be safe, explicitly set value to zero - forces = torques = 0.0 + logger.warning( + "The function 'set_external_force_and_torque' will be deprecated in a future release. Please" + " use 'permanent_wrench_composer.set_forces_and_torques' instead." + ) + + if forces is None and torques is None: + logger.warning("No forces or torques provided. No permanent external wrench will be applied.") # resolve all indices # -- env_ids if env_ids is None: - env_ids = self._ALL_ENV_INDICES + env_ids = self._ALL_ENV_INDICES_WP + elif not isinstance(env_ids, torch.Tensor): + env_ids = wp.array(env_ids, dtype=wp.int32, device=self.device) + else: + env_ids = wp.from_torch(env_ids.to(torch.int32), dtype=wp.int32) # -- object_ids if object_ids is None: - object_ids = self._ALL_OBJ_INDICES - # set into internal buffers - self._external_force_b[env_ids[:, None], object_ids] = forces - self._external_torque_b[env_ids[:, None], object_ids] = torques - - if is_global != self._use_global_wrench_frame: - omni.log.warn( - f"The external wrench frame has been changed from {self._use_global_wrench_frame} to {is_global}. This" - " may lead to unexpected behavior." + object_ids = self._ALL_OBJ_INDICES_WP + elif isinstance(object_ids, slice): + object_ids = wp.from_torch( + torch.arange(self.num_objects, dtype=torch.int32, device=self.device)[object_ids], dtype=wp.int32 ) - self._use_global_wrench_frame = is_global - - if positions is not None: - self.uses_external_wrench_positions = True - self._external_wrench_positions_b[env_ids[:, None], object_ids] = positions + elif not isinstance(object_ids, torch.Tensor): + object_ids = wp.array(object_ids, dtype=wp.int32, device=self.device) else: - if self.uses_external_wrench_positions: - self._external_wrench_positions_b[env_ids[:, None], object_ids] = 0.0 + object_ids = wp.from_torch(object_ids.to(torch.int32), dtype=wp.int32) + + # Write to wrench composer + self._permanent_wrench_composer.set_forces_and_torques( + forces=wp.from_torch(forces, dtype=wp.vec3f) if forces is not None else None, + torques=wp.from_torch(torques, dtype=wp.vec3f) if torques is not None else None, + positions=wp.from_torch(positions, dtype=wp.vec3f) if positions is not None else None, + body_ids=object_ids, + env_ids=env_ids, + is_global=is_global, + ) """ Helper functions. @@ -590,6 +618,8 @@ def reshape_data_to_view(self, data: torch.Tensor) -> torch.Tensor: """ def _initialize_impl(self): + # clear object names list to prevent double counting on re-initialization + self._object_names_list.clear() # obtain global simulation view self._physics_sim_view = SimulationManager.get_physics_sim_view() root_prim_path_exprs = [] @@ -648,9 +678,9 @@ def _initialize_impl(self): raise RuntimeError("Failed to create rigid body collection. Please check PhysX logs.") # log information about the rigid body - omni.log.info(f"Number of instances: {self.num_instances}") - omni.log.info(f"Number of distinct objects: {self.num_objects}") - omni.log.info(f"Object names: {self.object_names}") + logger.info(f"Number of instances: {self.num_instances}") + logger.info(f"Number of distinct objects: {self.num_objects}") + logger.info(f"Object names: {self.object_names}") # container for data access self._data = RigidObjectCollectionData(self.root_physx_view, self.num_objects, self.device) @@ -667,14 +697,12 @@ def _create_buffers(self): # constants self._ALL_ENV_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) self._ALL_OBJ_INDICES = torch.arange(self.num_objects, dtype=torch.long, device=self.device) + self._ALL_ENV_INDICES_WP = wp.from_torch(self._ALL_ENV_INDICES.to(torch.int32), dtype=wp.int32) + self._ALL_OBJ_INDICES_WP = wp.from_torch(self._ALL_OBJ_INDICES.to(torch.int32), dtype=wp.int32) - # external forces and torques - self.has_external_wrench = False - self._external_force_b = torch.zeros((self.num_instances, self.num_objects, 3), device=self.device) - self._external_torque_b = torch.zeros_like(self._external_force_b) - self._external_wrench_positions_b = torch.zeros_like(self._external_force_b) - self.uses_external_wrench_positions = False - self._use_global_wrench_frame = False + # external wrench composer + self._instantaneous_wrench_composer = WrenchComposer(self) + self._permanent_wrench_composer = WrenchComposer(self) # set information about rigid body into data self._data.object_names = self.object_names diff --git a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_cfg.py b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_cfg.py index 60a56d01e704..c99b20044dd5 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_cfg.py +++ b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py index 328010bb14f6..5156ef729e42 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py +++ b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py @@ -1,15 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch import weakref +import torch + import omni.physics.tensors.impl.api as physx import isaaclab.utils.math as math_utils -from isaaclab.sim.utils import get_current_stage_id +from isaaclab.sim.utils.stage import get_current_stage_id from isaaclab.utils.buffers import TimestampedBuffer diff --git a/source/isaaclab/isaaclab/assets/surface_gripper/__init__.py b/source/isaaclab/isaaclab/assets/surface_gripper/__init__.py index ed819fb8b71d..3786976617c3 100644 --- a/source/isaaclab/isaaclab/assets/surface_gripper/__init__.py +++ b/source/isaaclab/isaaclab/assets/surface_gripper/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py index 50a17d85efeb..2742e9baeb4c 100644 --- a/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py +++ b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py @@ -1,26 +1,30 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging import warnings from typing import TYPE_CHECKING -import omni.log +import torch + from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.version import get_version import isaaclab.sim as sim_utils from isaaclab.assets import AssetBase +from isaaclab.utils.version import get_isaac_sim_version if TYPE_CHECKING: from isaacsim.robot.surface_gripper import GripperView from .surface_gripper_cfg import SurfaceGripperCfg +# import logger +logger = logging.getLogger(__name__) + class SurfaceGripper(AssetBase): """A surface gripper actuator class. @@ -55,12 +59,11 @@ def __init__(self, cfg: SurfaceGripperCfg): # copy the configuration self._cfg = cfg.copy() - isaac_sim_version = get_version() # checks for Isaac Sim v5.0 to ensure that the surface gripper is supported - if int(isaac_sim_version[2]) < 5: - raise Exception( - "SurfaceGrippers are only supported by IsaacSim 5.0 and newer. Use IsaacSim 5.0 or newer to use this" - " feature." + if get_isaac_sim_version().major < 5: + raise NotImplementedError( + "SurfaceGrippers are only supported by IsaacSim 5.0 and newer. Current version is" + f" '{get_isaac_sim_version()}'. Please update to IsaacSim 5.0 or newer to use this feature." ) # flag for whether the sensor is initialized @@ -315,8 +318,8 @@ def _initialize_impl(self) -> None: ) # log information about the surface gripper - omni.log.info(f"Surface gripper initialized at: {self._cfg.prim_path} with root '{gripper_prim_path_expr}'.") - omni.log.info(f"Number of instances: {self._num_envs}") + logger.info(f"Surface gripper initialized at: {self._cfg.prim_path} with root '{gripper_prim_path_expr}'.") + logger.info(f"Number of instances: {self._num_envs}") # Reset grippers self.reset() diff --git a/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper_cfg.py b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper_cfg.py index 4a1f07738cc9..6648e183e2fb 100644 --- a/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper_cfg.py +++ b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/assets/utils/__init__.py b/source/isaaclab/isaaclab/assets/utils/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab/isaaclab/assets/utils/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab/isaaclab/controllers/__init__.py b/source/isaaclab/isaaclab/controllers/__init__.py index ffc5a5fb9a77..3a055c508e8d 100644 --- a/source/isaaclab/isaaclab/controllers/__init__.py +++ b/source/isaaclab/isaaclab/controllers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/controllers/config/__init__.py b/source/isaaclab/isaaclab/controllers/config/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab/isaaclab/controllers/config/__init__.py +++ b/source/isaaclab/isaaclab/controllers/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/controllers/config/rmp_flow.py b/source/isaaclab/isaaclab/controllers/config/rmp_flow.py index f3d214168fbb..55c6d8e1fbaa 100644 --- a/source/isaaclab/isaaclab/controllers/config/rmp_flow.py +++ b/source/isaaclab/isaaclab/controllers/config/rmp_flow.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,12 +10,18 @@ from isaaclab.controllers.rmp_flow import RmpFlowControllerCfg from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +# Directory on Nucleus Server for RMP-Flow assets (URDFs, collision models, etc.) ISAACLAB_NUCLEUS_RMPFLOW_DIR = os.path.join(ISAACLAB_NUCLEUS_DIR, "Controllers", "RmpFlowAssets") # Note: RMP-Flow config files for supported robots are stored in the motion_generation extension -_RMP_CONFIG_DIR = os.path.join( - get_extension_path_from_name("isaacsim.robot_motion.motion_generation"), "motion_policy_configs" -) +# We need to move it here for doc building purposes. +try: + _RMP_CONFIG_DIR = os.path.join( + get_extension_path_from_name("isaacsim.robot_motion.motion_generation"), + "motion_policy_configs", + ) +except Exception: + _RMP_CONFIG_DIR = "" # Path to current directory _CUR_DIR = os.path.dirname(os.path.realpath(__file__)) diff --git a/source/isaaclab/isaaclab/controllers/differential_ik.py b/source/isaaclab/isaaclab/controllers/differential_ik.py index a9bbfdb160c7..8841dbe4fb5e 100644 --- a/source/isaaclab/isaaclab/controllers/differential_ik.py +++ b/source/isaaclab/isaaclab/controllers/differential_ik.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.utils.math import apply_delta_pose, compute_pose_error if TYPE_CHECKING: @@ -209,7 +210,7 @@ def _compute_delta_joint_pos(self, delta_pose: torch.Tensor, jacobian: torch.Ten # U: 6xd, S: dxd, V: d x num-joint U, S, Vh = torch.linalg.svd(jacobian) S_inv = 1.0 / S - S_inv = torch.where(S > min_singular_value, S_inv, torch.zeros_like(S_inv)) + S_inv = torch.where(min_singular_value < S, S_inv, torch.zeros_like(S_inv)) jacobian_pinv = ( torch.transpose(Vh, dim0=1, dim1=2)[:, :, :6] @ torch.diag_embed(S_inv) diff --git a/source/isaaclab/isaaclab/controllers/differential_ik_cfg.py b/source/isaaclab/isaaclab/controllers/differential_ik_cfg.py index 3a79474305d6..315a762752ca 100644 --- a/source/isaaclab/isaaclab/controllers/differential_ik_cfg.py +++ b/source/isaaclab/isaaclab/controllers/differential_ik_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/controllers/joint_impedance.py b/source/isaaclab/isaaclab/controllers/joint_impedance.py index d0db23d6d99e..bd35089b81af 100644 --- a/source/isaaclab/isaaclab/controllers/joint_impedance.py +++ b/source/isaaclab/isaaclab/controllers/joint_impedance.py @@ -1,12 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from collections.abc import Sequence from dataclasses import MISSING +import torch + from isaaclab.utils import configclass diff --git a/source/isaaclab/isaaclab/controllers/operational_space.py b/source/isaaclab/isaaclab/controllers/operational_space.py index e9f07fcbe12d..2505768e0581 100644 --- a/source/isaaclab/isaaclab/controllers/operational_space.py +++ b/source/isaaclab/isaaclab/controllers/operational_space.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.utils.math import ( apply_delta_pose, combine_frame_transforms, @@ -509,7 +510,6 @@ def compute( # Null space position control if self.cfg.nullspace_control == "position": - # Check if the current joint positions and velocities are provided if current_joint_pos is None or current_joint_vel is None: raise ValueError("Current joint positions and velocities are required for null-space control.") diff --git a/source/isaaclab/isaaclab/controllers/operational_space_cfg.py b/source/isaaclab/isaaclab/controllers/operational_space_cfg.py index 8e9cf3ba9d46..d2fc3575bd73 100644 --- a/source/isaaclab/isaaclab/controllers/operational_space_cfg.py +++ b/source/isaaclab/isaaclab/controllers/operational_space_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/controllers/pink_ik/__init__.py b/source/isaaclab/isaaclab/controllers/pink_ik/__init__.py index 005645f97985..17ed7a67b077 100644 --- a/source/isaaclab/isaaclab/controllers/pink_ik/__init__.py +++ b/source/isaaclab/isaaclab/controllers/pink_ik/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/controllers/pink_ik/local_frame_task.py b/source/isaaclab/isaaclab/controllers/pink_ik/local_frame_task.py index e46174bcaa50..ff8c6b9b03d8 100644 --- a/source/isaaclab/isaaclab/controllers/pink_ik/local_frame_task.py +++ b/source/isaaclab/isaaclab/controllers/pink_ik/local_frame_task.py @@ -1,11 +1,11 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np from collections.abc import Sequence +import numpy as np import pinocchio as pin from pink.tasks.frame_task import FrameTask diff --git a/source/isaaclab/isaaclab/controllers/pink_ik/null_space_posture_task.py b/source/isaaclab/isaaclab/controllers/pink_ik/null_space_posture_task.py index 212071c904e8..8ab6ddcc2dc0 100644 --- a/source/isaaclab/isaaclab/controllers/pink_ik/null_space_posture_task.py +++ b/source/isaaclab/isaaclab/controllers/pink_ik/null_space_posture_task.py @@ -1,11 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np - import pinocchio as pin +import scipy.linalg.blas as blas +import scipy.linalg.lapack as lapack from pink.configuration import Configuration from pink.tasks import Task @@ -39,7 +40,8 @@ class NullSpacePostureTask(Task): .. math:: - \mathbf{J}_{\text{posture}}(\mathbf{q}) = \mathbf{N}(\mathbf{q}) = \mathbf{I} - \mathbf{J}_{\text{primary}}^+ \mathbf{J}_{\text{primary}} + \mathbf{J}_{\text{posture}}(\mathbf{q}) = \mathbf{N}(\mathbf{q}) = + \mathbf{I} -\mathbf{J}_{\text{primary}}^+ \mathbf{J}_{\text{primary}} where: - :math:`\mathbf{J}_{\text{primary}}` is the combined Jacobian of all higher priority tasks @@ -57,7 +59,8 @@ class NullSpacePostureTask(Task): \mathbf{J}_2(\mathbf{q}) \end{bmatrix} - where :math:`\mathbf{J}_1(\mathbf{q})` and :math:`\mathbf{J}_2(\mathbf{q})` are the Jacobians for the first and second frame tasks, respectively. + where :math:`\mathbf{J}_1(\mathbf{q})` and :math:`\mathbf{J}_2(\mathbf{q})` are the Jacobians for the + first and second frame tasks, respectively. The null space projector ensures that joint velocities in the null space produce zero velocity for the primary tasks: :math:`\mathbf{J}_{\text{primary}} \cdot \dot{\mathbf{q}}_{\text{null}} = \mathbf{0}`. @@ -68,13 +71,18 @@ class NullSpacePostureTask(Task): .. math:: - \left\| \mathbf{N}(\mathbf{q}) \mathbf{v} + \mathbf{M} \cdot (\mathbf{q}^* - \mathbf{q}) \right\|_{W_{\text{posture}}}^2 + \left\| + \mathbf{N}(\mathbf{q}) \mathbf{v} + \mathbf{M} \cdot (\mathbf{q}^* - \mathbf{q}) + \right\|_{W_{\text{posture}}}^2 This formulation allows the robot to maintain a desired posture while respecting the constraints imposed by higher priority tasks (e.g., end-effector positioning). """ + # Regularization factor for pseudoinverse computation to ensure numerical stability + PSEUDOINVERSE_DAMPING_FACTOR: float = 1e-9 + def __init__( self, cost: float, @@ -213,7 +221,8 @@ def compute_jacobian(self, configuration: Configuration) -> np.ndarray: - :math:`\mathbf{I}` is the identity matrix The null space projector ensures that joint velocities in the null space produce - zero velocity for the primary tasks: :math:`\mathbf{J}_{\text{primary}} \cdot \dot{\mathbf{q}}_{\text{null}} = \mathbf{0}`. + zero velocity for the primary tasks: + :math:`\mathbf{J}_{\text{primary}} \cdot \dot{\mathbf{q}}_{\text{null}} = \mathbf{0}`. If no controlled frames are specified, returns the identity matrix. @@ -237,6 +246,30 @@ def compute_jacobian(self, configuration: Configuration) -> np.ndarray: J_combined = np.concatenate(J_frame_tasks, axis=0) # Compute null space projector: N = I - J^+ * J - N_combined = np.eye(J_combined.shape[1]) - np.linalg.pinv(J_combined) @ J_combined + # Use fast pseudoinverse computation with direct LAPACK/BLAS calls + m, n = J_combined.shape + + # Wide matrix (typical for robotics): use left pseudoinverse + # J^+ = J^T @ inv(J @ J^T + λ²I) + # This is faster because we invert an m×m matrix instead of n×n + + # Compute J @ J^T using BLAS (faster than numpy) + JJT = blas.dgemm(1.0, J_combined, J_combined.T) + np.fill_diagonal(JJT, JJT.diagonal() + self.PSEUDOINVERSE_DAMPING_FACTOR**2) + + # Use LAPACK's Cholesky factorization (dpotrf = Positive definite TRiangular Factorization) + L, info = lapack.dpotrf(JJT, lower=1, clean=False, overwrite_a=True) + + if info != 0: + # Fallback if not positive definite: use numpy's pseudoinverse + J_pinv = np.linalg.pinv(J_combined) + return np.eye(n) - J_pinv @ J_combined + + # Solve (J @ J^T + λ²I) @ X = J using LAPACK's triangular solver (dpotrs) + # This directly solves the system without computing the full inverse + X, _ = lapack.dpotrs(L, J_combined, lower=1) + + # Compute null space projector: N = I - J^T @ X + N_combined = np.eye(n) - J_combined.T @ X return N_combined diff --git a/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik.py b/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik.py index 344244d141b9..788a5da67053 100644 --- a/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik.py +++ b/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,10 +14,10 @@ from __future__ import annotations -import numpy as np -import torch from typing import TYPE_CHECKING +import numpy as np +import torch from pink import solve_ik from isaaclab.assets import ArticulationCfg @@ -39,7 +39,8 @@ class PinkIKController: Multiple tasks are resolved through weighted optimization, formulating a quadratic program that minimizes weighted task errors while respecting joint velocity limits. - It supports user defined tasks, and we have provided a NullSpacePostureTask for maintaining desired joint configurations. + It supports user defined tasks, and we have provided a NullSpacePostureTask for maintaining desired + joint configurations. Reference: Pink IK Solver: https://github.com/stephane-caron/pink @@ -221,13 +222,12 @@ def compute( self.pink_configuration, self.cfg.variable_input_tasks + self.cfg.fixed_input_tasks, dt, - solver="osqp", + solver="daqp", safety_break=self.cfg.fail_on_joint_limit_violation, ) joint_angle_changes = velocity * dt except (AssertionError, Exception) as e: # Print warning and return the current joint positions as the target - # Not using omni.log since its not available in CI during docs build if self.cfg.show_ik_warnings: print( "Warning: IK quadratic solver could not find a solution! Did not update the target joint" diff --git a/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik_cfg.py b/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik_cfg.py index ed7e40b0c480..a66c4aec6658 100644 --- a/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik_cfg.py +++ b/source/isaaclab/isaaclab/controllers/pink_ik/pink_ik_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -20,18 +20,26 @@ class PinkIKControllerCfg: """ urdf_path: str | None = None - """Path to the robot's URDF file. This file is used by Pinocchio's `robot_wrapper.BuildFromURDF` to load the robot model.""" + """Path to the robot's URDF file. This file is used by Pinocchio's ``robot_wrapper.BuildFromURDF`` + to load the robot model. + """ mesh_path: str | None = None - """Path to the mesh files associated with the robot. These files are also loaded by Pinocchio's `robot_wrapper.BuildFromURDF`.""" + """Path to the mesh files associated with the robot. These files are also loaded by Pinocchio's + ``robot_wrapper.BuildFromURDF``. + """ num_hand_joints: int = 0 - """The number of hand joints in the robot. The action space for the controller contains the pose_dim(7)*num_controlled_frames + num_hand_joints. - The last num_hand_joints values of the action are the hand joint angles.""" + """The number of hand joints in the robot. - variable_input_tasks: list[FrameTask] = MISSING + The action space for the controller contains the ``pose_dim(7) * num_controlled_frames + num_hand_joints``. + The last ``num_hand_joints`` values of the action are the hand joint angles. """ - A list of tasks for the Pink IK controller. These tasks are controllable by the env action. + + variable_input_tasks: list[FrameTask] = MISSING + """A list of tasks for the Pink IK controller. + + These tasks are controllable by the environment action. These tasks can be used to control the pose of a frame or the angles of joints. For more details, visit: https://github.com/stephane-caron/pink @@ -46,12 +54,18 @@ class PinkIKControllerCfg: """ joint_names: list[str] | None = None - """A list of joint names in the USD asset controlled by the Pink IK controller. This is required because the joint naming conventions differ between USD and URDF files. - This value is currently designed to be automatically populated by the action term in a manager based environment.""" + """A list of joint names in the USD asset controlled by the Pink IK controller. + + This is required because the joint naming conventions differ between USD and URDF files. This value is + currently designed to be automatically populated by the action term in a manager based environment. + """ all_joint_names: list[str] | None = None - """A list of joint names in the USD asset. This is required because the joint naming conventions differ between USD and URDF files. - This value is currently designed to be automatically populated by the action term in a manager based environment.""" + """A list of joint names in the USD asset. + + This is required because the joint naming conventions differ between USD and URDF files. This value is + currently designed to be automatically populated by the action term in a manager based environment. + """ articulation_name: str = "robot" """The name of the articulation USD asset in the scene.""" @@ -63,9 +77,13 @@ class PinkIKControllerCfg: """Show warning if IK solver fails to find a solution.""" fail_on_joint_limit_violation: bool = True - """If True, the Pink IK solver will fail and raise an error if any joint limit is violated during optimization. PinkIKController - will handle the error by setting the last joint positions. If False, the solver will ignore joint limit violations and return the - closest solution found.""" + """Whether to fail on joint limit violation. + + If True, the Pink IK solver will fail and raise an error if any joint limit is violated during optimization. + The PinkIKController will handle the error by setting the last joint positions. + + If False, the solver will ignore joint limit violations and return the closest solution found. + """ xr_enabled: bool = False """If True, the Pink IK controller will send information to the XRVisualization.""" diff --git a/source/isaaclab/isaaclab/controllers/pink_ik/pink_kinematics_configuration.py b/source/isaaclab/isaaclab/controllers/pink_ik/pink_kinematics_configuration.py index 6bc11c5f1987..cd935390f0aa 100644 --- a/source/isaaclab/isaaclab/controllers/pink_ik/pink_kinematics_configuration.py +++ b/source/isaaclab/isaaclab/controllers/pink_ik/pink_kinematics_configuration.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,7 +6,6 @@ from __future__ import annotations import numpy as np - import pinocchio as pin from pink.configuration import Configuration from pink.exceptions import FrameNotFound @@ -18,11 +17,13 @@ class PinkKinematicsConfiguration(Configuration): A configuration class that maintains both a "controlled" (reduced) model and a "full" model. This class extends the standard Pink Configuration to allow for selective joint control: - - The "controlled" model/data/q represent the subset of joints being actively controlled (e.g., a kinematic chain or arm). + + - The "controlled" model/data/q represent the subset of joints being actively controlled + (e.g., a kinematic chain or arm). - The "full" model/data/q represent the complete robot, including all joints. - This is useful for scenarios where only a subset of joints are being optimized or controlled, but full-model kinematics - (e.g., for collision checking, full-body Jacobians, or visualization) are still required. + This is useful for scenarios where only a subset of joints are being optimized or controlled, but + full-model kinematics (e.g., for collision checking, full-body Jacobians, or visualization) are still required. The class ensures that both models are kept up to date, and provides methods to update both the controlled and full configurations as needed. @@ -39,16 +40,19 @@ def __init__( """ Initialize PinkKinematicsConfiguration. + + This constructor initializes the PinkKinematicsConfiguration, which maintains both a "controlled" + (reduced) model and a "full" model. The controlled model/data/q represent the subset of joints + being actively controlled, while the full model/data/q represent the complete robot. This is useful + for scenarios where only a subset of joints are being optimized or controlled, but full-model + kinematics are still required. + Args: - urdf_path (str): Path to the robot URDF file. - mesh_path (str): Path to the mesh files for the robot. - controlled_joint_names (list[str]): List of joint names to be actively controlled. - copy_data (bool, optional): If True, work on an internal copy of the input data. Defaults to True. - forward_kinematics (bool, optional): If True, compute forward kinematics from the configuration vector. Defaults to True. - - This constructor initializes the PinkKinematicsConfiguration, which maintains both a "controlled" (reduced) model and a "full" model. - The controlled model/data/q represent the subset of joints being actively controlled, while the full model/data/q represent the complete robot. - This is useful for scenarios where only a subset of joints are being optimized or controlled, but full-model kinematics are still required. + urdf_path: Path to the robot URDF file. + mesh_path: Path to the mesh files for the robot. + controlled_joint_names: List of joint names to be actively controlled. + copy_data: If True, work on an internal copy of the input data. Defaults to True. + forward_kinematics: If True, compute forward kinematics from the configuration vector. Defaults to True. """ self._controlled_joint_names = controlled_joint_names @@ -63,7 +67,8 @@ def __init__( # import pdb; pdb.set_trace() self._all_joint_names = self.full_model.names.tolist()[1:] - # controlled_joint_indices: indices in all_joint_names for joints that are in controlled_joint_names, preserving all_joint_names order + # controlled_joint_indices: indices in all_joint_names for joints that are in controlled_joint_names, + # preserving all_joint_names order self._controlled_joint_indices = [ idx for idx, joint_name in enumerate(self._all_joint_names) if joint_name in self._controlled_joint_names ] @@ -146,9 +151,11 @@ def get_frame_jacobian(self, frame: str) -> np.ndarray: def get_transform_frame_to_world(self, frame: str) -> pin.SE3: """Get the pose of a frame in the current configuration. + We override this method from the super class to solve the issue that in the default Pink implementation, the frame placements do not take into account the non-controlled joints - being not at initial pose (which is a bad assumption when they are controlled by other controllers like a lower body controller). + being not at initial pose (which is a bad assumption when they are controlled by other + controllers like a lower body controller). Args: frame: Name of a frame, typically a link name from the URDF. diff --git a/source/isaaclab/isaaclab/controllers/rmp_flow.py b/source/isaaclab/isaaclab/controllers/rmp_flow.py index b9ce875c390c..f2766d702418 100644 --- a/source/isaaclab/isaaclab/controllers/rmp_flow.py +++ b/source/isaaclab/isaaclab/controllers/rmp_flow.py @@ -1,13 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from dataclasses import MISSING -import isaacsim.core.utils.prims as prim_utils -from isaacsim.core.api.simulation_context import SimulationContext +import torch + from isaacsim.core.prims import SingleArticulation # enable motion generation extensions @@ -19,6 +18,7 @@ from isaacsim.robot_motion.motion_generation import ArticulationMotionPolicy from isaacsim.robot_motion.motion_generation.lula.motion_policies import RmpFlow, RmpFlowSmoothed +import isaaclab.sim as sim_utils from isaaclab.utils import configclass from isaaclab.utils.assets import retrieve_file_path @@ -79,9 +79,9 @@ def initialize(self, prim_paths_expr: str): prim_paths_expr: The expression to find the articulation prim paths. """ # obtain the simulation time - physics_dt = SimulationContext.instance().get_physics_dt() + physics_dt = sim_utils.SimulationContext.instance().get_physics_dt() # find all prims - self._prim_paths = prim_utils.find_matching_prim_paths(prim_paths_expr) + self._prim_paths = sim_utils.find_matching_prim_paths(prim_paths_expr) self.num_robots = len(self._prim_paths) # resolve controller if self.cfg.name == "rmp_flow": diff --git a/source/isaaclab/isaaclab/controllers/utils.py b/source/isaaclab/isaaclab/controllers/utils.py index b674b267acbb..7e72912fdfda 100644 --- a/source/isaaclab/isaaclab/controllers/utils.py +++ b/source/isaaclab/isaaclab/controllers/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ This module provides utility functions to help with controller implementations. """ +import logging import os import re @@ -15,10 +16,11 @@ enable_extension("isaacsim.asset.exporter.urdf") -import nvidia.srl.tools.logger as logger -import omni.log from nvidia.srl.from_usd.to_urdf import UsdToUrdf +# import logger +logger = logging.getLogger(__name__) + def convert_usd_to_urdf(usd_path: str, output_path: str, force_conversion: bool = True) -> tuple[str, str]: """Convert a USD file to URDF format. @@ -35,7 +37,7 @@ def convert_usd_to_urdf(usd_path: str, output_path: str, force_conversion: bool "edge_names_to_remove": None, "root": None, "parent_link_is_body_1": None, - "log_level": logger.level_from_name("ERROR"), + "log_level": logging.ERROR, } urdf_output_dir = os.path.join(output_path, "urdf") @@ -90,11 +92,11 @@ def change_revolute_to_fixed(urdf_path: str, fixed_joints: list[str], verbose: b old_str = f'' new_str = f'' if verbose: - omni.log.warn(f"Replacing {joint} with fixed joint") - omni.log.warn(old_str) - omni.log.warn(new_str) + logger.warning(f"Replacing {joint} with fixed joint") + logger.warning(old_str) + logger.warning(new_str) if old_str not in content: - omni.log.warn(f"Error: Could not find revolute joint named '{joint}' in URDF file") + logger.warning(f"Error: Could not find revolute joint named '{joint}' in URDF file") content = content.replace(old_str, new_str) with open(urdf_path, "w") as file: @@ -127,9 +129,9 @@ def change_revolute_to_fixed_regex(urdf_path: str, fixed_joints: list[str], verb old_str = f'' new_str = f'' if verbose: - omni.log.warn(f"Replacing {joint} with fixed joint") - omni.log.warn(old_str) - omni.log.warn(new_str) + logger.warning(f"Replacing {joint} with fixed joint") + logger.warning(old_str) + logger.warning(new_str) content = content.replace(old_str, new_str) with open(urdf_path, "w") as file: diff --git a/source/isaaclab/isaaclab/devices/__init__.py b/source/isaaclab/isaaclab/devices/__init__.py index 718695e3503a..b2605d39ca16 100644 --- a/source/isaaclab/isaaclab/devices/__init__.py +++ b/source/isaaclab/isaaclab/devices/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +11,7 @@ * **Spacemouse**: 3D mouse with 6 degrees of freedom. * **Gamepad**: Gamepad with 2D two joysticks and buttons. Example: Xbox controller. * **OpenXR**: Uses hand tracking of index/thumb tip avg to drive the target pose. Gripping is done with pinching. +* **Haply**: Haptic device (Inverse3 + VerseGrip) with position, orientation tracking and force feedback. All device interfaces inherit from the :class:`DeviceBase` class, which provides a common interface for all devices. The device interface reads the input data when @@ -21,6 +22,7 @@ from .device_base import DeviceBase, DeviceCfg, DevicesCfg from .gamepad import Se2Gamepad, Se2GamepadCfg, Se3Gamepad, Se3GamepadCfg +from .haply import HaplyDevice, HaplyDeviceCfg from .keyboard import Se2Keyboard, Se2KeyboardCfg, Se3Keyboard, Se3KeyboardCfg from .openxr import ManusVive, ManusViveCfg, OpenXRDevice, OpenXRDeviceCfg from .retargeter_base import RetargeterBase, RetargeterCfg diff --git a/source/isaaclab/isaaclab/devices/device_base.py b/source/isaaclab/isaaclab/devices/device_base.py index b7955468cc11..a434bcc73cfd 100644 --- a/source/isaaclab/isaaclab/devices/device_base.py +++ b/source/isaaclab/isaaclab/devices/device_base.py @@ -1,16 +1,18 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Base class for teleoperation interface.""" -import torch from abc import ABC, abstractmethod from collections.abc import Callable from dataclasses import dataclass, field +from enum import Enum from typing import Any +import torch + from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg @@ -18,8 +20,14 @@ class DeviceCfg: """Configuration for teleoperation devices.""" + # Whether teleoperation should start active by default + teleoperation_active_default: bool = True + # Torch device string to place output tensors on sim_device: str = "cpu" + # Retargeters that transform device data into robot commands retargeters: list[RetargeterCfg] = field(default_factory=list) + # Concrete device class to construct for this config. Set by each device module. + class_type: type["DeviceBase"] | None = None @dataclass @@ -52,9 +60,13 @@ def __init__(self, retargeters: list[RetargeterBase] | None = None): """ # Initialize empty list if None is provided self._retargeters = retargeters or [] + # Aggregate required features across all retargeters + self._required_features = set() + for retargeter in self._retargeters: + self._required_features.update(retargeter.get_requirements()) def __str__(self) -> str: - """Returns: A string containing the information of joystick.""" + """Returns: A string identifier for the device.""" return f"{self.__class__.__name__}" """ @@ -117,3 +129,32 @@ def advance(self) -> torch.Tensor: # With multiple retargeters, return a tuple of outputs # Concatenate retargeted outputs into a single tensor return torch.cat([retargeter.retarget(raw_data) for retargeter in self._retargeters], dim=-1) + + # ----------------------------- + # Shared data layout helpers (for retargeters across devices) + # ----------------------------- + class TrackingTarget(Enum): + """Standard tracking targets shared across devices.""" + + HAND_LEFT = 0 + HAND_RIGHT = 1 + HEAD = 2 + CONTROLLER_LEFT = 3 + CONTROLLER_RIGHT = 4 + + class MotionControllerDataRowIndex(Enum): + """Rows in the motion-controller 2x7 array.""" + + POSE = 0 + INPUTS = 1 + + class MotionControllerInputIndex(Enum): + """Indices in the motion-controller input row.""" + + THUMBSTICK_X = 0 + THUMBSTICK_Y = 1 + TRIGGER = 2 + SQUEEZE = 3 + BUTTON_0 = 4 + BUTTON_1 = 5 + PADDING = 6 diff --git a/source/isaaclab/isaaclab/devices/gamepad/__init__.py b/source/isaaclab/isaaclab/devices/gamepad/__init__.py index 41a1b88bb3de..8f8ec66aa4ec 100644 --- a/source/isaaclab/isaaclab/devices/gamepad/__init__.py +++ b/source/isaaclab/isaaclab/devices/gamepad/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py b/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py index dacf1cdb497d..5954c3c6918e 100644 --- a/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py +++ b/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py @@ -1,16 +1,19 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Gamepad controller for SE(2) control.""" -import numpy as np -import torch +from __future__ import annotations + import weakref from collections.abc import Callable from dataclasses import dataclass +import numpy as np +import torch + import carb import carb.input import omni @@ -18,16 +21,6 @@ from ..device_base import DeviceBase, DeviceCfg -@dataclass -class Se2GamepadCfg(DeviceCfg): - """Configuration for SE2 gamepad devices.""" - - v_x_sensitivity: float = 1.0 - v_y_sensitivity: float = 1.0 - omega_z_sensitivity: float = 1.0 - dead_zone: float = 0.01 - - class Se2Gamepad(DeviceBase): r"""A gamepad controller for sending SE(2) commands as velocity commands. @@ -209,3 +202,14 @@ def _resolve_command_buffer(self, raw_command: np.ndarray) -> np.ndarray: command[command_sign] *= -1 return command + + +@dataclass +class Se2GamepadCfg(DeviceCfg): + """Configuration for SE2 gamepad devices.""" + + v_x_sensitivity: float = 1.0 + v_y_sensitivity: float = 1.0 + omega_z_sensitivity: float = 1.0 + dead_zone: float = 0.01 + class_type: type[DeviceBase] = Se2Gamepad diff --git a/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py b/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py index f30dc8357e09..2520de6247eb 100644 --- a/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py +++ b/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py @@ -1,15 +1,18 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Gamepad controller for SE(3) control.""" -import numpy as np -import torch +from __future__ import annotations + import weakref from collections.abc import Callable from dataclasses import dataclass + +import numpy as np +import torch from scipy.spatial.transform import Rotation import carb @@ -18,17 +21,6 @@ from ..device_base import DeviceBase, DeviceCfg -@dataclass -class Se3GamepadCfg(DeviceCfg): - """Configuration for SE3 gamepad devices.""" - - gripper_term: bool = True - dead_zone: float = 0.01 # For gamepad devices - pos_sensitivity: float = 1.0 - rot_sensitivity: float = 1.6 - retargeters: None = None - - class Se3Gamepad(DeviceBase): """A gamepad controller for sending SE(3) commands as delta poses and binary command (open/close). @@ -264,3 +256,14 @@ def _resolve_command_buffer(self, raw_command: np.ndarray) -> np.ndarray: delta_command[delta_command_sign] *= -1 return delta_command + + +@dataclass +class Se3GamepadCfg(DeviceCfg): + """Configuration for SE3 gamepad devices.""" + + gripper_term: bool = True + dead_zone: float = 0.01 # For gamepad devices + pos_sensitivity: float = 1.0 + rot_sensitivity: float = 1.6 + class_type: type[DeviceBase] = Se3Gamepad diff --git a/source/isaaclab/isaaclab/devices/haply/__init__.py b/source/isaaclab/isaaclab/devices/haply/__init__.py new file mode 100644 index 000000000000..b86030f80e7a --- /dev/null +++ b/source/isaaclab/isaaclab/devices/haply/__init__.py @@ -0,0 +1,10 @@ +# 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 + +"""Haply device interface for teleoperation.""" + +from .se3_haply import HaplyDevice, HaplyDeviceCfg + +__all__ = ["HaplyDevice", "HaplyDeviceCfg"] diff --git a/source/isaaclab/isaaclab/devices/haply/se3_haply.py b/source/isaaclab/isaaclab/devices/haply/se3_haply.py new file mode 100644 index 000000000000..9d9c06c92dc7 --- /dev/null +++ b/source/isaaclab/isaaclab/devices/haply/se3_haply.py @@ -0,0 +1,395 @@ +# 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 + +"""Haply device controller for SE3 control with force feedback.""" + +from __future__ import annotations + +import asyncio +import json +import threading +import time +from collections.abc import Callable +from dataclasses import dataclass + +import numpy as np +import torch + +try: + import websockets + + WEBSOCKETS_AVAILABLE = True +except ImportError: + WEBSOCKETS_AVAILABLE = False + +from ..device_base import DeviceBase, DeviceCfg +from ..retargeter_base import RetargeterBase + + +class HaplyDevice(DeviceBase): + """A Haply device controller for sending SE(3) commands with force feedback. + + This class provides an interface to Haply robotic devices (Inverse3 + VerseGrip) + for teleoperation. It communicates via WebSocket and supports: + + - Position tracking from Inverse3 device + - Orientation and button inputs from VerseGrip device + - Directional force feedback to Inverse3 + - Real-time data streaming at configurable rates + + The device provides raw data: + + * Position: 3D position (x, y, z) in meters from Inverse3 + * Orientation: Quaternion (x, y, z, w) from VerseGrip + * Buttons: Three buttons (a, b, c) from VerseGrip with state (pressed/not pressed) + + Note: All button logic (e.g., gripper control, reset, mode switching) should be + implemented in the application layer using the raw button states from advance(). + + Note: + Requires the Haply SDK to be running and accessible via WebSocket. + Install dependencies: pip install websockets + + """ + + def __init__(self, cfg: HaplyDeviceCfg, retargeters: list[RetargeterBase] | None = None): + """Initialize the Haply device interface. + + Args: + cfg: Configuration object for Haply device settings. + retargeters: Optional list of retargeting components that transform device data + into robot commands. If None or empty, the device outputs its native data format. + + Raises: + ImportError: If websockets module is not installed. + RuntimeError: If connection to Haply device fails. + """ + super().__init__(retargeters) + + if not WEBSOCKETS_AVAILABLE: + raise ImportError("websockets module is required for Haply device. Install with: pip install websockets") + + # Store configuration + self.websocket_uri = cfg.websocket_uri + self.pos_sensitivity = cfg.pos_sensitivity + self.data_rate = cfg.data_rate + self._sim_device = cfg.sim_device + self.limit_force = cfg.limit_force + + # Device status (True only when both Inverse3 and VerseGrip are connected) + self.connected = False + self._connected_lock = threading.Lock() + + # Device IDs (will be set after first message) + self.inverse3_device_id = None + self.verse_grip_device_id = None + + # Current data cache + self.cached_data = { + "position": np.zeros(3, dtype=np.float32), + "quaternion": np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32), + "buttons": {"a": False, "b": False, "c": False}, + "inverse3_connected": False, + "versegrip_connected": False, + } + + self.data_lock = threading.Lock() + + # Force feedback + self.feedback_force = {"x": 0.0, "y": 0.0, "z": 0.0} + self.force_lock = threading.Lock() + + self._additional_callbacks = dict() + + # Button state tracking + self._prev_buttons = {"a": False, "b": False, "c": False} + + # Connection monitoring + self.consecutive_timeouts = 0 + self.max_consecutive_timeouts = 10 # ~10 seconds at 1s timeout + self.timeout_warning_issued = False + + # Start WebSocket connection + self.running = True + self._websocket_thread = None + self._start_websocket_thread() + + # Wait for both devices to connect + timeout = 5.0 + start_time = time.time() + while (time.time() - start_time) < timeout: + with self._connected_lock: + if self.connected: + break + time.sleep(0.1) + + with self._connected_lock: + if not self.connected: + raise RuntimeError(f"Failed to connect both Inverse3 and VerseGrip devices within {timeout}s. ") + + def __del__(self): + """Cleanup on deletion: shutdown WebSocket connection and background thread.""" + if not hasattr(self, "running") or not self.running: + return + + self.running = False + + # Reset force feedback before closing + if hasattr(self, "force_lock") and hasattr(self, "feedback_force"): + with self.force_lock: + self.feedback_force = {"x": 0.0, "y": 0.0, "z": 0.0} + + # Explicitly wait for WebSocket thread to finish + if hasattr(self, "_websocket_thread") and self._websocket_thread is not None: + if self._websocket_thread.is_alive(): + self._websocket_thread.join(timeout=2.0) + if self._websocket_thread.is_alive(): + self._websocket_thread.daemon = True + + def __str__(self) -> str: + """Returns: A string containing the information of the device.""" + msg = f"Haply Device Controller: {self.__class__.__name__}\n" + msg += f"\tWebSocket URI: {self.websocket_uri}\n" + msg += f"\tInverse3 ID: {self.inverse3_device_id}\n" + msg += f"\tVerseGrip ID: {self.verse_grip_device_id}\n" + msg += "\t----------------------------------------------\n" + msg += "\tOutput: [x, y, z, qx, qy, qz, qw, btn_a, btn_b, btn_c]\n" + msg += "\tInverse3: Provides position (x, y, z) and force feedback\n" + msg += "\tVerseGrip: Provides orientation (quaternion) and buttons (a, b, c)" + return msg + + def reset(self): + """Reset the device internal state.""" + with self.force_lock: + self.feedback_force = {"x": 0.0, "y": 0.0, "z": 0.0} + + # Reset button state tracking + self._prev_buttons = {"a": False, "b": False, "c": False} + + def add_callback(self, key: str, func: Callable): + """Add additional functions to bind to button events. + + Args: + key: The button to check against. Valid values are "a", "b", "c". + func: The function to call when button is pressed. The callback function should not + take any arguments. + """ + if key not in ["a", "b", "c"]: + raise ValueError(f"Invalid button key: {key}. Valid keys are 'a', 'b', 'c'.") + self._additional_callbacks[key] = func + + def advance(self) -> torch.Tensor: + """Provides the result from Haply device state. + + Returns: + torch.Tensor: A tensor containing the raw device data: + - 10 elements: [x, y, z, qx, qy, qz, qw, button_a, button_b, button_c] + where (x, y, z) is position, (qx, qy, qz, qw) is quaternion orientation, + and buttons are 1.0 (pressed) or 0.0 (not pressed) + """ + with self.data_lock: + if not (self.cached_data["inverse3_connected"] and self.cached_data["versegrip_connected"]): + raise RuntimeError("Haply devices not connected. Both Inverse3 and VerseGrip must be connected.") + + # Safe copy within lock + position = self.cached_data["position"].copy() * self.pos_sensitivity + quaternion = self.cached_data["quaternion"].copy() + button_a = self.cached_data["buttons"].get("a", False) + button_b = self.cached_data["buttons"].get("b", False) + button_c = self.cached_data["buttons"].get("c", False) + + # Button callbacks execute OUTSIDE lock to prevent deadlock + for button_key, current_state in [("a", button_a), ("b", button_b), ("c", button_c)]: + prev_state = self._prev_buttons.get(button_key, False) + + if current_state and not prev_state: + if button_key in self._additional_callbacks: + self._additional_callbacks[button_key]() + + self._prev_buttons[button_key] = current_state + + button_states = np.array( + [ + 1.0 if button_a else 0.0, + 1.0 if button_b else 0.0, + 1.0 if button_c else 0.0, + ], + dtype=np.float32, + ) + + # Construct command tensor: [position(3), quaternion(4), buttons(3)] + command = np.concatenate([position, quaternion, button_states]) + + return torch.tensor(command, dtype=torch.float32, device=self._sim_device) + + def push_force(self, forces: torch.Tensor, position: torch.Tensor) -> None: + """Push force vector to Haply Inverse3 device. + + Overrides DeviceBase.push_force() to provide force feedback for Haply Inverse3. + Forces are clipped to [-limit_force, limit_force] range for safety. + + Args: + forces: Tensor of shape (N, 3) with forces [fx, fy, fz]. + position: Tensor of shape (N) with indices specifying which forces to use. + """ + # Check if forces is empty + if forces.shape[0] == 0: + raise ValueError("No forces provided") + + # Select forces using position indices + selected_forces = forces[position] if position.ndim > 0 else forces[position].unsqueeze(0) + force = selected_forces.sum(dim=0) + force = force.cpu().numpy() if force.is_cuda else force.numpy() + + fx = np.clip(force[0], -self.limit_force, self.limit_force) + fy = np.clip(force[1], -self.limit_force, self.limit_force) + fz = np.clip(force[2], -self.limit_force, self.limit_force) + + with self.force_lock: + self.feedback_force = {"x": float(fx), "y": float(fy), "z": float(fz)} + + def _start_websocket_thread(self): + """Start WebSocket connection thread.""" + + def websocket_thread(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(self._websocket_loop()) + + self._websocket_thread = threading.Thread(target=websocket_thread, daemon=False) + self._websocket_thread.start() + + async def _websocket_loop(self): + """WebSocket data reading and writing loop.""" + while self.running: + try: + async with websockets.connect(self.websocket_uri, ping_interval=None, ping_timeout=None) as ws: + first_message = True + + while self.running: + try: + response = await asyncio.wait_for(ws.recv(), timeout=1.0) + data = json.loads(response) + + self.consecutive_timeouts = 0 + if self.timeout_warning_issued: + self.timeout_warning_issued = False + + # Safe array access - no IndexError risk with ternary operator + inverse3_list = data.get("inverse3", []) + verse_grip_list = data.get("wireless_verse_grip", []) + inverse3_data = inverse3_list[0] if inverse3_list else {} + verse_grip_data = verse_grip_list[0] if verse_grip_list else {} + + if first_message: + first_message = False + if inverse3_data: + self.inverse3_device_id = inverse3_data.get("device_id") + if verse_grip_data: + self.verse_grip_device_id = verse_grip_data.get("device_id") + + with self.data_lock: + inverse3_connected = False + versegrip_connected = False + + if inverse3_data and "state" in inverse3_data: + cursor_pos = inverse3_data["state"].get("cursor_position", {}) + if cursor_pos: + self.cached_data["position"] = np.array( + [cursor_pos.get(k, 0.0) for k in ("x", "y", "z")], dtype=np.float32 + ) + inverse3_connected = True + + if verse_grip_data and "state" in verse_grip_data: + state = verse_grip_data["state"] + self.cached_data["buttons"] = { + k: state.get("buttons", {}).get(k, False) for k in ("a", "b", "c") + } + orientation = state.get("orientation", {}) + if orientation: + self.cached_data["quaternion"] = np.array( + [ + orientation.get(k, 1.0 if k == "w" else 0.0) + for k in ("x", "y", "z", "w") + ], + dtype=np.float32, + ) + versegrip_connected = True + + self.cached_data["inverse3_connected"] = inverse3_connected + self.cached_data["versegrip_connected"] = versegrip_connected + # Both devices required (AND logic): Inverse3 for position/force, + both_connected = inverse3_connected and versegrip_connected + + with self._connected_lock: + self.connected = both_connected + + # Send force feedback + if self.inverse3_device_id: + with self.force_lock: + current_force = self.feedback_force.copy() + + request_msg = { + "inverse3": [ + { + "device_id": self.inverse3_device_id, + "commands": {"set_cursor_force": {"values": current_force}}, + } + ] + } + await ws.send(json.dumps(request_msg)) + + await asyncio.sleep(1.0 / self.data_rate) + + except asyncio.TimeoutError: + self.consecutive_timeouts += 1 + + # Check if timeout + if ( + self.consecutive_timeouts >= self.max_consecutive_timeouts + and not self.timeout_warning_issued + ): + self.timeout_warning_issued = True + with self.data_lock: + self.cached_data["inverse3_connected"] = False + self.cached_data["versegrip_connected"] = False + with self._connected_lock: + self.connected = False + continue + except Exception as e: + print(f"[ERROR] Error in WebSocket receive loop: {e}") + break + + except Exception: + with self.data_lock: + self.cached_data["inverse3_connected"] = False + self.cached_data["versegrip_connected"] = False + with self._connected_lock: + self.connected = False + self.consecutive_timeouts = 0 + self.timeout_warning_issued = False + + if self.running: + await asyncio.sleep(2.0) + else: + break + + +@dataclass +class HaplyDeviceCfg(DeviceCfg): + """Configuration for Haply device. + + Attributes: + websocket_uri: WebSocket URI for Haply SDK connection + pos_sensitivity: Position sensitivity scaling factor + data_rate: Data exchange rate in Hz + limit_force: Maximum force magnitude in Newtons (safety limit) + """ + + websocket_uri: str = "ws://localhost:10001" + pos_sensitivity: float = 1.0 + data_rate: float = 200.0 + limit_force: float = 2.0 + class_type: type[DeviceBase] = HaplyDevice diff --git a/source/isaaclab/isaaclab/devices/keyboard/__init__.py b/source/isaaclab/isaaclab/devices/keyboard/__init__.py index 1f210c577b5f..eff757a6d132 100644 --- a/source/isaaclab/isaaclab/devices/keyboard/__init__.py +++ b/source/isaaclab/isaaclab/devices/keyboard/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py b/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py index 45edf1145b77..beb19d1835d7 100644 --- a/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py +++ b/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py @@ -1,31 +1,25 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Keyboard controller for SE(2) control.""" -import numpy as np -import torch +from __future__ import annotations + import weakref from collections.abc import Callable from dataclasses import dataclass +import numpy as np +import torch + import carb import omni from ..device_base import DeviceBase, DeviceCfg -@dataclass -class Se2KeyboardCfg(DeviceCfg): - """Configuration for SE2 keyboard devices.""" - - v_x_sensitivity: float = 0.8 - v_y_sensitivity: float = 0.4 - omega_z_sensitivity: float = 1.0 - - class Se2Keyboard(DeviceBase): r"""A keyboard controller for sending SE(2) commands as velocity commands. @@ -178,3 +172,13 @@ def _create_key_bindings(self): "NUMPAD_9": np.asarray([0.0, 0.0, -1.0]) * self.omega_z_sensitivity, "X": np.asarray([0.0, 0.0, -1.0]) * self.omega_z_sensitivity, } + + +@dataclass +class Se2KeyboardCfg(DeviceCfg): + """Configuration for SE2 keyboard devices.""" + + v_x_sensitivity: float = 0.8 + v_y_sensitivity: float = 0.4 + omega_z_sensitivity: float = 1.0 + class_type: type[DeviceBase] = Se2Keyboard diff --git a/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py b/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py index 94b654e8b176..db6b17d1702a 100644 --- a/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py +++ b/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py @@ -1,15 +1,18 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Keyboard controller for SE(3) control.""" -import numpy as np -import torch +from __future__ import annotations + import weakref from collections.abc import Callable from dataclasses import dataclass + +import numpy as np +import torch from scipy.spatial.transform import Rotation import carb @@ -18,16 +21,6 @@ from ..device_base import DeviceBase, DeviceCfg -@dataclass -class Se3KeyboardCfg(DeviceCfg): - """Configuration for SE3 keyboard devices.""" - - gripper_term: bool = True - pos_sensitivity: float = 0.4 - rot_sensitivity: float = 0.8 - retargeters: None = None - - class Se3Keyboard(DeviceBase): """A keyboard controller for sending SE(3) commands as delta poses and binary command (open/close). @@ -206,3 +199,14 @@ def _create_key_bindings(self): "C": np.asarray([0.0, 0.0, 1.0]) * self.rot_sensitivity, "V": np.asarray([0.0, 0.0, -1.0]) * self.rot_sensitivity, } + + +@dataclass +class Se3KeyboardCfg(DeviceCfg): + """Configuration for SE3 keyboard devices.""" + + gripper_term: bool = True + pos_sensitivity: float = 0.4 + rot_sensitivity: float = 0.8 + retargeters: None = None + class_type: type[DeviceBase] = Se3Keyboard diff --git a/source/isaaclab/isaaclab/devices/openxr/__init__.py b/source/isaaclab/isaaclab/devices/openxr/__init__.py index e7bc0cfda038..030fdbdd00b5 100644 --- a/source/isaaclab/isaaclab/devices/openxr/__init__.py +++ b/source/isaaclab/isaaclab/devices/openxr/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,4 +7,4 @@ from .manus_vive import ManusVive, ManusViveCfg from .openxr_device import OpenXRDevice, OpenXRDeviceCfg -from .xr_cfg import XrCfg, remove_camera_configs +from .xr_cfg import XrAnchorRotationMode, XrCfg, remove_camera_configs diff --git a/source/isaaclab/isaaclab/devices/openxr/common.py b/source/isaaclab/isaaclab/devices/openxr/common.py index 0dd667033361..088641c2886a 100644 --- a/source/isaaclab/isaaclab/devices/openxr/common.py +++ b/source/isaaclab/isaaclab/devices/openxr/common.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/manus_vive.py b/source/isaaclab/isaaclab/devices/openxr/manus_vive.py index 4dda777843f2..f8a18a098a8c 100644 --- a/source/isaaclab/isaaclab/devices/openxr/manus_vive.py +++ b/source/isaaclab/isaaclab/devices/openxr/manus_vive.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,20 +7,22 @@ Manus and Vive for teleoperation and interaction. """ +from __future__ import annotations + import contextlib -import numpy as np from collections.abc import Callable from dataclasses import dataclass -from enum import Enum + +import numpy as np +from packaging import version import carb -from isaacsim.core.version import get_version from isaaclab.devices.openxr.common import HAND_JOINT_NAMES from isaaclab.devices.retargeter_base import RetargeterBase +from isaaclab.utils.version import get_isaac_sim_version from ..device_base import DeviceBase, DeviceCfg -from .openxr_device import OpenXRDevice from .xr_cfg import XrCfg # For testing purposes, we need to mock the XRCore @@ -34,13 +36,6 @@ from .manus_vive_utils import HAND_JOINT_MAP, ManusViveIntegration -@dataclass -class ManusViveCfg(DeviceCfg): - """Configuration for Manus and Vive.""" - - xr_cfg: XrCfg | None = None - - class ManusVive(DeviceBase): """Manus gloves and Vive trackers for teleoperation and interaction. @@ -66,13 +61,6 @@ class ManusVive(DeviceBase): data is transformed into robot control commands suitable for teleoperation. """ - class TrackingTarget(Enum): - """Enum class specifying what to track with Manus+Vive. Consistent with :class:`OpenXRDevice.TrackingTarget`.""" - - HAND_LEFT = 0 - HAND_RIGHT = 1 - HEAD = 2 - TELEOP_COMMAND_EVENT_TYPE = "teleop_command" def __init__(self, cfg: ManusViveCfg, retargeters: list[RetargeterBase] | None = None): @@ -84,10 +72,9 @@ def __init__(self, cfg: ManusViveCfg, retargeters: list[RetargeterBase] | None = """ super().__init__(retargeters) # Enforce minimum Isaac Sim version (>= 5.1) - version_info = get_version() - major, minor = int(version_info[2]), int(version_info[3]) - if (major < 5) or (major == 5 and minor < 1): - raise RuntimeError(f"ManusVive requires Isaac Sim >= 5.1. Detected version {major}.{minor}. ") + isaac_sim_version = get_isaac_sim_version() + if isaac_sim_version < version.parse("5.1"): + raise RuntimeError(f"ManusVive requires Isaac Sim >= 5.1. Detected version: '{isaac_sim_version}'.") self._xr_cfg = cfg.xr_cfg or XrCfg() self._additional_callbacks = dict() self._vc_subscription = ( @@ -197,9 +184,9 @@ def _get_raw_data(self) -> dict: joint_name = HAND_JOINT_MAP[int(index)] result[hand][joint_name] = np.array(pose["position"] + pose["orientation"], dtype=np.float32) return { - OpenXRDevice.TrackingTarget.HAND_LEFT: result["left"], - OpenXRDevice.TrackingTarget.HAND_RIGHT: result["right"], - OpenXRDevice.TrackingTarget.HEAD: self._calculate_headpose(), + DeviceBase.TrackingTarget.HAND_LEFT: result["left"], + DeviceBase.TrackingTarget.HAND_RIGHT: result["right"], + DeviceBase.TrackingTarget.HEAD: self._calculate_headpose(), } def _calculate_headpose(self) -> np.ndarray: @@ -217,15 +204,17 @@ def _calculate_headpose(self) -> np.ndarray: quatw = quat.GetReal() # Store in w, x, y, z order to match our convention - self._previous_headpose = np.array([ - position[0], - position[1], - position[2], - quatw, - quati[0], - quati[1], - quati[2], - ]) + self._previous_headpose = np.array( + [ + position[0], + position[1], + position[2], + quatw, + quati[0], + quati[1], + quati[2], + ] + ) return self._previous_headpose @@ -246,3 +235,11 @@ def _on_teleop_command(self, event: carb.events.IEvent): elif "reset" in msg: if "RESET" in self._additional_callbacks: self._additional_callbacks["RESET"]() + + +@dataclass +class ManusViveCfg(DeviceCfg): + """Configuration for Manus and Vive.""" + + xr_cfg: XrCfg | None = None + class_type: type[DeviceBase] = ManusVive diff --git a/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py b/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py index 3044579136e0..80c0b346b315 100644 --- a/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py +++ b/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 contextlib -import numpy as np +import logging from time import time -import omni.log +import numpy as np + from isaacsim.core.utils.extensions import enable_extension # For testing purposes, we need to mock the XRCore @@ -18,6 +19,9 @@ from pxr import Gf +# import logger +logger = logging.getLogger(__name__) + # Mapping from Manus joint index (0-24) to joint name. Palm (25) is calculated from middle metacarpal and proximal. HAND_JOINT_MAP = { # Wrist @@ -144,13 +148,14 @@ def update_vive(self): if self.scene_T_lighthouse_static is None: self._initialize_coordinate_transformation() except Exception as e: - omni.log.error(f"Vive tracker update failed: {e}") + logger.error(f"Vive tracker update failed: {e}") def _initialize_coordinate_transformation(self): - """ - Initialize the scene to lighthouse coordinate transformation. - The coordinate transformation is used to transform the wrist pose from lighthouse coordinate system to isaac sim scene coordinate. - It is computed from multiple frames of AVP/OpenXR wrist pose and Vive wrist pose samples at the beginning of the session. + """Initialize the scene to lighthouse coordinate transformation. + + The coordinate transformation is used to transform the wrist pose from lighthouse + coordinate system to isaac sim scene coordinate. It is computed from multiple + frames of AVP/OpenXR wrist pose and Vive wrist pose samples at the beginning of the session. """ min_frames = 6 tolerance = 3.0 @@ -216,7 +221,7 @@ def _initialize_coordinate_transformation(self): choose_A = False elif len(self._pairA_trans_errs) % 10 == 0 or len(self._pairB_trans_errs) % 10 == 0: print("Computing pairing of Vive trackers with wrists") - omni.log.info( + logger.info( f"Pairing Vive trackers with wrists: error of pairing A: {errA}, error of pairing B: {errB}" ) if choose_A is None: @@ -245,7 +250,7 @@ def _initialize_coordinate_transformation(self): ) except Exception as e: - omni.log.error(f"Failed to initialize coordinate transformation: {e}") + logger.error(f"Failed to initialize coordinate transformation: {e}") def _transform_vive_data(self, device_data: dict) -> dict: """Transform Vive tracker poses to scene coordinates. @@ -433,7 +438,7 @@ def get_openxr_wrist_matrix(hand: str) -> Gf.Matrix4d: return None return joint.pose_matrix except Exception as e: - omni.log.warn(f"OpenXR {hand} wrist fetch failed: {e}") + logger.warning(f"OpenXR {hand} wrist fetch failed: {e}") return None diff --git a/source/isaaclab/isaaclab/devices/openxr/openxr_device.py b/source/isaaclab/isaaclab/devices/openxr/openxr_device.py index 4e5e08249803..49f423fe8a01 100644 --- a/source/isaaclab/isaaclab/devices/openxr/openxr_device.py +++ b/source/isaaclab/isaaclab/devices/openxr/openxr_device.py @@ -1,39 +1,41 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """OpenXR-powered device for teleoperation and interaction.""" +from __future__ import annotations + import contextlib -import numpy as np +import logging from collections.abc import Callable from dataclasses import dataclass -from enum import Enum from typing import Any +import numpy as np + import carb +# import logger +logger = logging.getLogger(__name__) + from isaaclab.devices.openxr.common import HAND_JOINT_NAMES from isaaclab.devices.retargeter_base import RetargeterBase from ..device_base import DeviceBase, DeviceCfg +from .xr_anchor_utils import XrAnchorSynchronizer from .xr_cfg import XrCfg # For testing purposes, we need to mock the XRCore, XRPoseValidityFlags classes XRCore = None XRPoseValidityFlags = None +XRCoreEventType = None with contextlib.suppress(ModuleNotFoundError): - from omni.kit.xr.core import XRCore, XRPoseValidityFlags -from isaacsim.core.prims import SingleXFormPrim - - -@dataclass -class OpenXRDeviceCfg(DeviceCfg): - """Configuration for OpenXR devices.""" + from omni.kit.xr.core import XRCore, XRCoreEventType, XRPoseValidityFlags - xr_cfg: XrCfg | None = None +from isaacsim.core.prims import SingleXFormPrim class OpenXRDevice(DeviceBase): @@ -64,19 +66,6 @@ class OpenXRDevice(DeviceBase): data is transformed into robot control commands suitable for teleoperation. """ - class TrackingTarget(Enum): - """Enum class specifying what to track with OpenXR. - - Attributes: - HAND_LEFT: Track the left hand (index 0 in _get_raw_data output) - HAND_RIGHT: Track the right hand (index 1 in _get_raw_data output) - HEAD: Track the head/headset position (index 2 in _get_raw_data output) - """ - - HAND_LEFT = 0 - HAND_RIGHT = 1 - HEAD = 2 - TELEOP_COMMAND_EVENT_TYPE = "teleop_command" def __init__( @@ -93,12 +82,13 @@ def __init__( super().__init__(retargeters) self._xr_cfg = cfg.xr_cfg or XrCfg() self._additional_callbacks = dict() + self._xr_core = XRCore.get_singleton() if XRCore is not None else None self._vc_subscription = ( - XRCore.get_singleton() - .get_message_bus() - .create_subscription_to_pop_by_type( + self._xr_core.get_message_bus().create_subscription_to_pop_by_type( carb.events.type_from_string(self.TELEOP_COMMAND_EVENT_TYPE), self._on_teleop_command ) + if self._xr_core is not None + else None ) # Initialize dictionaries instead of arrays @@ -107,10 +97,57 @@ def __init__( self._previous_joint_poses_right = {name: default_pose.copy() for name in HAND_JOINT_NAMES} self._previous_headpose = default_pose.copy() - xr_anchor = SingleXFormPrim("/XRAnchor", position=self._xr_cfg.anchor_pos, orientation=self._xr_cfg.anchor_rot) - carb.settings.get_settings().set_float("/persistent/xr/profile/ar/render/nearPlane", self._xr_cfg.near_plane) - carb.settings.get_settings().set_string("/persistent/xr/profile/ar/anchorMode", "custom anchor") - carb.settings.get_settings().set_string("/xrstage/profile/ar/customAnchor", xr_anchor.prim_path) + if self._xr_cfg.anchor_prim_path is not None: + anchor_path = self._xr_cfg.anchor_prim_path + if anchor_path.endswith("/"): + anchor_path = anchor_path[:-1] + self._xr_anchor_headset_path = f"{anchor_path}/XRAnchor" + else: + self._xr_anchor_headset_path = "/World/XRAnchor" + + _ = SingleXFormPrim( + self._xr_anchor_headset_path, position=self._xr_cfg.anchor_pos, orientation=self._xr_cfg.anchor_rot + ) + + if hasattr(carb, "settings"): + carb.settings.get_settings().set_float( + "/persistent/xr/profile/ar/render/nearPlane", self._xr_cfg.near_plane + ) + carb.settings.get_settings().set_string("/persistent/xr/profile/ar/anchorMode", "custom anchor") + carb.settings.get_settings().set_string("/xrstage/profile/ar/customAnchor", self._xr_anchor_headset_path) + + # Button binding support + self.__button_subscriptions: dict[str, dict] = {} + + # Optional anchor synchronizer + self._anchor_sync: XrAnchorSynchronizer | None = None + if self._xr_core is not None and self._xr_cfg.anchor_prim_path is not None: + try: + self._anchor_sync = XrAnchorSynchronizer( + xr_core=self._xr_core, + xr_cfg=self._xr_cfg, + xr_anchor_headset_path=self._xr_anchor_headset_path, + ) + # Subscribe to pre_sync_update to keep anchor in sync + if XRCoreEventType is not None: + self._xr_pre_sync_update_subscription = ( + self._xr_core.get_message_bus().create_subscription_to_pop_by_type( + XRCoreEventType.pre_sync_update, + lambda _: self._anchor_sync.sync_headset_to_anchor(), + name="isaaclab_xr_pre_sync_update", + ) + ) + except Exception as e: + logger.warning(f"XR: Failed to initialize anchor synchronizer: {e}") + + # Default convenience binding: toggle anchor rotation with right controller 'a' button + with contextlib.suppress(Exception): + self._bind_button_press( + "/user/hand/right", + "a", + "isaaclab_right_a", + lambda ev: self._toggle_anchor_rotation(), + ) def __del__(self): """Clean up resources when the object is destroyed. @@ -120,6 +157,11 @@ def __del__(self): """ if hasattr(self, "_vc_subscription") and self._vc_subscription is not None: self._vc_subscription = None + if hasattr(self, "_xr_pre_sync_update_subscription") and self._xr_pre_sync_update_subscription is not None: + self._xr_pre_sync_update_subscription = None + # clear button subscriptions + if hasattr(self, "__button_subscriptions"): + self._unbind_all_buttons() # No need to explicitly clean up OpenXR instance as it's managed by NVIDIA Isaac Sim @@ -136,6 +178,10 @@ def __str__(self) -> str: msg = f"OpenXR Hand Tracking Device: {self.__class__.__name__}\n" msg += f"\tAnchor Position: {self._xr_cfg.anchor_pos}\n" msg += f"\tAnchor Rotation: {self._xr_cfg.anchor_rot}\n" + if self._xr_cfg.anchor_prim_path is not None: + msg += f"\tAnchor Prim Path: {self._xr_cfg.anchor_prim_path} (Dynamic Anchoring)\n" + else: + msg += "\tAnchor Mode: Static (Root Level)\n" # Add retargeter information if self._retargeters: @@ -176,6 +222,8 @@ def reset(self): self._previous_joint_poses_left = {name: default_pose.copy() for name in HAND_JOINT_NAMES} self._previous_joint_poses_right = {name: default_pose.copy() for name in HAND_JOINT_NAMES} self._previous_headpose = default_pose.copy() + if hasattr(self, "_anchor_sync") and self._anchor_sync is not None: + self._anchor_sync.reset() def add_callback(self, key: str, func: Callable): """Add additional functions to bind to client messages. @@ -199,17 +247,37 @@ def _get_raw_data(self) -> Any: Each pose is represented as a 7-element array: [x, y, z, qw, qx, qy, qz] where the first 3 elements are position and the last 4 are quaternion orientation. """ - return { - self.TrackingTarget.HAND_LEFT: self._calculate_joint_poses( + data = {} + + if RetargeterBase.Requirement.HAND_TRACKING in self._required_features: + data[DeviceBase.TrackingTarget.HAND_LEFT] = self._calculate_joint_poses( XRCore.get_singleton().get_input_device("/user/hand/left"), self._previous_joint_poses_left, - ), - self.TrackingTarget.HAND_RIGHT: self._calculate_joint_poses( + ) + data[DeviceBase.TrackingTarget.HAND_RIGHT] = self._calculate_joint_poses( XRCore.get_singleton().get_input_device("/user/hand/right"), self._previous_joint_poses_right, - ), - self.TrackingTarget.HEAD: self._calculate_headpose(), - } + ) + + if RetargeterBase.Requirement.HEAD_TRACKING in self._required_features: + data[DeviceBase.TrackingTarget.HEAD] = self._calculate_headpose() + + if RetargeterBase.Requirement.MOTION_CONTROLLER in self._required_features: + # Optionally include motion controller pose+inputs if devices are available + try: + left_dev = XRCore.get_singleton().get_input_device("/user/hand/left") + right_dev = XRCore.get_singleton().get_input_device("/user/hand/right") + left_ctrl = self._query_controller(left_dev) if left_dev is not None else np.array([]) + right_ctrl = self._query_controller(right_dev) if right_dev is not None else np.array([]) + if left_ctrl.size: + data[DeviceBase.TrackingTarget.CONTROLLER_LEFT] = left_ctrl + if right_ctrl.size: + data[DeviceBase.TrackingTarget.CONTROLLER_RIGHT] = right_ctrl + except Exception: + # Ignore controller data if XRCore/controller APIs are unavailable + pass + + return data """ Internal helpers. @@ -279,18 +347,147 @@ def _calculate_headpose(self) -> np.ndarray: quatw = quat.GetReal() # Store in w, x, y, z order to match our convention - self._previous_headpose = np.array([ - position[0], - position[1], - position[2], - quatw, - quati[0], - quati[1], - quati[2], - ]) + self._previous_headpose = np.array( + [ + position[0], + position[1], + position[2], + quatw, + quati[0], + quati[1], + quati[2], + ] + ) return self._previous_headpose + # ----------------------------- + # Controller button binding utilities + # ----------------------------- + def _bind_button_press( + self, + device_path: str, + button_name: str, + event_name: str, + on_button_press: Callable[[carb.events.IEvent], None], + ) -> None: + if self._xr_core is None: + logger.warning("XR core not available; skipping button binding") + return + + sub_key = f"{device_path}/{button_name}" + self.__button_subscriptions[sub_key] = {} + + def try_emit_button_events(): + if self.__button_subscriptions[sub_key].get("generator"): + return + device = self._xr_core.get_input_device(device_path) + if not device: + return + names = {str(n) for n in (device.get_input_names() or ())} + if button_name not in names: + return + gen = device.bind_event_generator(button_name, event_name, ("press",)) + if gen is not None: + logger.info(f"XR: Bound event generator for {sub_key}, {event_name}") + self.__button_subscriptions[sub_key]["generator"] = gen + + def on_inputs_change(_ev: carb.events.IEvent) -> None: + try_emit_button_events() + + def on_disable(_ev: carb.events.IEvent) -> None: + self.__button_subscriptions[sub_key]["generator"] = None + + message_bus = self._xr_core.get_message_bus() + event_suffix = device_path.strip("/").replace("/", "_") + self.__button_subscriptions[sub_key]["press_sub"] = message_bus.create_subscription_to_pop_by_type( + carb.events.type_from_string(f"{event_name}.press"), on_button_press + ) + self.__button_subscriptions[sub_key]["inputs_change_sub"] = message_bus.create_subscription_to_pop_by_type( + carb.events.type_from_string(f"xr_input.{event_suffix}.inputs_change"), on_inputs_change + ) + self.__button_subscriptions[sub_key]["disable_sub"] = message_bus.create_subscription_to_pop_by_type( + carb.events.type_from_string(f"xr_input.{event_suffix}.disable"), on_disable + ) + try_emit_button_events() + + def _unbind_all_buttons(self) -> None: + for sub_key, subs in self.__button_subscriptions.items(): + if "generator" in subs: + subs["generator"] = None + for key in ["press_sub", "inputs_change_sub", "disable_sub"]: + if key in subs: + subs[key] = None + self.__button_subscriptions.clear() + logger.info("XR: Unbound all button event handlers") + + def _toggle_anchor_rotation(self): + if self._anchor_sync is not None: + self._anchor_sync.toggle_anchor_rotation() + + def _query_controller(self, input_device) -> np.ndarray: + """Query motion controller pose and inputs as a 2x7 array. + + Row 0 (POSE): [x, y, z, w, x, y, z] + Row 1 (INPUTS): [thumbstick_x, thumbstick_y, trigger, squeeze, button_0, button_1, padding] + """ + if input_device is None: + return np.array([]) + + pose = input_device.get_virtual_world_pose() + position = pose.ExtractTranslation() + quat = pose.ExtractRotationQuat() + + thumbstick_x = 0.0 + thumbstick_y = 0.0 + trigger = 0.0 + squeeze = 0.0 + button_0 = 0.0 + button_1 = 0.0 + + if input_device.has_input_gesture("thumbstick", "x"): + thumbstick_x = float(input_device.get_input_gesture_value("thumbstick", "x")) + if input_device.has_input_gesture("thumbstick", "y"): + thumbstick_y = float(input_device.get_input_gesture_value("thumbstick", "y")) + if input_device.has_input_gesture("trigger", "value"): + trigger = float(input_device.get_input_gesture_value("trigger", "value")) + if input_device.has_input_gesture("squeeze", "value"): + squeeze = float(input_device.get_input_gesture_value("squeeze", "value")) + + # Determine which button pair exists on this device + if input_device.has_input_gesture("x", "click") or input_device.has_input_gesture("y", "click"): + if input_device.has_input_gesture("x", "click"): + button_0 = float(input_device.get_input_gesture_value("x", "click")) + if input_device.has_input_gesture("y", "click"): + button_1 = float(input_device.get_input_gesture_value("y", "click")) + else: + if input_device.has_input_gesture("a", "click"): + button_0 = float(input_device.get_input_gesture_value("a", "click")) + if input_device.has_input_gesture("b", "click"): + button_1 = float(input_device.get_input_gesture_value("b", "click")) + + pose_row = [ + position[0], + position[1], + position[2], + quat.GetReal(), + quat.GetImaginary()[0], + quat.GetImaginary()[1], + quat.GetImaginary()[2], + ] + + input_row = [ + thumbstick_x, + thumbstick_y, + trigger, + squeeze, + button_0, + button_1, + 0.0, + ] + + return np.array([pose_row, input_row], dtype=np.float32) + def _on_teleop_command(self, event: carb.events.IEvent): msg = event.payload["message"] @@ -303,3 +500,12 @@ def _on_teleop_command(self, event: carb.events.IEvent): elif "reset" in msg: if "RESET" in self._additional_callbacks: self._additional_callbacks["RESET"]() + self.reset() + + +@dataclass +class OpenXRDeviceCfg(DeviceCfg): + """Configuration for OpenXR devices.""" + + xr_cfg: XrCfg | None = None + class_type: type[DeviceBase] = OpenXRDevice diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/__init__.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/__init__.py index f2972ec65805..94ef9c0e4e5f 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/__init__.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,7 +6,19 @@ from .humanoid.fourier.gr1t2_retargeter import GR1T2Retargeter, GR1T2RetargeterCfg from .humanoid.unitree.g1_lower_body_standing import G1LowerBodyStandingRetargeter, G1LowerBodyStandingRetargeterCfg +from .humanoid.unitree.g1_motion_controller_locomotion import ( + G1LowerBodyStandingMotionControllerRetargeter, + G1LowerBodyStandingMotionControllerRetargeterCfg, +) from .humanoid.unitree.inspire.g1_upper_body_retargeter import UnitreeG1Retargeter, UnitreeG1RetargeterCfg +from .humanoid.unitree.trihand.g1_upper_body_motion_ctrl_gripper import ( + G1TriHandUpperBodyMotionControllerGripperRetargeter, + G1TriHandUpperBodyMotionControllerGripperRetargeterCfg, +) +from .humanoid.unitree.trihand.g1_upper_body_motion_ctrl_retargeter import ( + G1TriHandUpperBodyMotionControllerRetargeter, + G1TriHandUpperBodyMotionControllerRetargeterCfg, +) from .humanoid.unitree.trihand.g1_upper_body_retargeter import ( G1TriHandUpperBodyRetargeter, G1TriHandUpperBodyRetargeterCfg, diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/dex/dex_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/dex/dex_retargeter.py deleted file mode 100644 index 2092728b2319..000000000000 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/dex/dex_retargeter.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2022-2025, 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 numpy as np -from typing import Any - -from isaaclab.devices.retargeter_base import RetargeterBase - - -class DexRetargeter(RetargeterBase): - """Retargets OpenXR hand joint data to DEX robot joint commands. - - This class implements the RetargeterBase interface to convert hand tracking data - into a format suitable for controlling DEX robot hands. - """ - - def __init__(self): - """Initialize the DEX retargeter.""" - super().__init__() - # TODO: Add any initialization parameters and state variables needed - pass - - def retarget(self, joint_data: dict[str, np.ndarray]) -> Any: - """Convert OpenXR hand joint poses to DEX robot commands. - - Args: - joint_data: Dictionary mapping OpenXR joint names to their pose data. - Each pose is a numpy array of shape (7,) containing - [x, y, z, qx, qy, qz, qw] for absolute mode or - [x, y, z, roll, pitch, yaw] for relative mode. - - Returns: - Retargeted data in the format expected by DEX robot control interface. - TODO: Specify the exact return type and format - """ - # TODO: Implement the retargeting logic - raise NotImplementedError("DexRetargeter.retarget() not implemented") diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_left_dexpilot.yml b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_left_dexpilot.yml index 6a98e472190c..1e203d11e7e8 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_left_dexpilot.yml +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_left_dexpilot.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_right_dexpilot.yml b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_right_dexpilot.yml index 183df868e8d9..f67041bd9b60 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_right_dexpilot.yml +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/data/configs/dex-retargeting/fourier_hand_right_dexpilot.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1_t2_dex_retargeting_utils.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1_t2_dex_retargeting_utils.py index c0a7b056e81f..aaeb9bda0314 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1_t2_dex_retargeting_utils.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1_t2_dex_retargeting_utils.py @@ -1,35 +1,42 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np +import logging import os + +import numpy as np import torch import yaml -from scipy.spatial.transform import Rotation as R - -import omni.log from dex_retargeting.retargeting_config import RetargetingConfig +from scipy.spatial.transform import Rotation as R from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path +# import logger +logger = logging.getLogger(__name__) + # The index to map the OpenXR hand joints to the hand joints used # in Dex-retargeting. _HAND_JOINTS_INDEX = [1, 2, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25] # The transformation matrices to convert hand pose to canonical view. -_OPERATOR2MANO_RIGHT = np.array([ - [0, -1, 0], - [-1, 0, 0], - [0, 0, -1], -]) - -_OPERATOR2MANO_LEFT = np.array([ - [0, -1, 0], - [-1, 0, 0], - [0, 0, -1], -]) +_OPERATOR2MANO_RIGHT = np.array( + [ + [0, -1, 0], + [-1, 0, 0], + [0, 0, -1], + ] +) + +_OPERATOR2MANO_LEFT = np.array( + [ + [0, -1, 0], + [-1, 0, 0], + [0, 0, -1], + ] +) _LEFT_HAND_JOINT_NAMES = [ "L_index_proximal_joint", @@ -104,7 +111,7 @@ def __init__( self.dof_names = self.left_dof_names + self.right_dof_names self.isaac_lab_hand_joint_names = hand_joint_names - omni.log.info("[GR1T2DexRetargeter] init done.") + logger.info("[GR1T2DexRetargeter] init done.") def _update_yaml_with_urdf_path(self, yaml_path: str, urdf_path: str): """Update YAML file with the correct URDF path. @@ -121,16 +128,16 @@ def _update_yaml_with_urdf_path(self, yaml_path: str, urdf_path: str): # Update the URDF path in the configuration if "retargeting" in config: config["retargeting"]["urdf_path"] = urdf_path - omni.log.info(f"Updated URDF path in {yaml_path} to {urdf_path}") + logger.info(f"Updated URDF path in {yaml_path} to {urdf_path}") else: - omni.log.warn(f"Unable to find 'retargeting' section in {yaml_path}") + logger.warning(f"Unable to find 'retargeting' section in {yaml_path}") # Write the updated configuration back to the file with open(yaml_path, "w") as file: yaml.dump(config, file) except Exception as e: - omni.log.error(f"Error updating YAML file {yaml_path}: {e}") + logger.error(f"Error updating YAML file {yaml_path}: {e}") def convert_hand_joints(self, hand_poses: dict[str, np.ndarray], operator2mano: np.ndarray) -> np.ndarray: """Prepares the hand joints data for retargeting. diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1t2_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1t2_retargeter.py index 4548c0f99cba..0f95d4b9d758 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1t2_retargeter.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/fourier/gr1t2_retargeter.py @@ -1,38 +1,34 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + import contextlib +from dataclasses import dataclass + import numpy as np import torch -from dataclasses import dataclass import isaaclab.sim as sim_utils import isaaclab.utils.math as PoseUtils -from isaaclab.devices import OpenXRDevice +from isaaclab.devices.device_base import DeviceBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg -# This import exception is suppressed because gr1_t2_dex_retargeting_utils depends on pinocchio which is not available on windows +# This import exception is suppressed because gr1_t2_dex_retargeting_utils depends +# on pinocchio which is not available on Windows. with contextlib.suppress(Exception): from .gr1_t2_dex_retargeting_utils import GR1TR2DexRetargeting -@dataclass -class GR1T2RetargeterCfg(RetargeterCfg): - """Configuration for the GR1T2 retargeter.""" - - enable_visualization: bool = False - num_open_xr_hand_joints: int = 100 - hand_joint_names: list[str] | None = None # List of robot hand joint names - - class GR1T2Retargeter(RetargeterBase): """Retargets OpenXR hand tracking data to GR1T2 hand end-effector commands. This retargeter maps hand tracking data from OpenXR to joint commands for the GR1T2 robot's hands. - It handles both left and right hands, converting poses of the hands in OpenXR format joint angles for the GR1T2 robot's hands. + It handles both left and right hands, converting poses of the hands in OpenXR format joint angles + for the GR1T2 robot's hands. """ def __init__( @@ -48,6 +44,7 @@ def __init__( hand_joint_names: List of robot hand joint names """ + super().__init__(cfg) self._hand_joint_names = cfg.hand_joint_names self._hands_controller = GR1TR2DexRetargeting(self._hand_joint_names) @@ -81,8 +78,8 @@ def retarget(self, data: dict) -> torch.Tensor: """ # Access the left and right hand data using the enum key - left_hand_poses = data[OpenXRDevice.TrackingTarget.HAND_LEFT] - right_hand_poses = data[OpenXRDevice.TrackingTarget.HAND_RIGHT] + left_hand_poses = data[DeviceBase.TrackingTarget.HAND_LEFT] + right_hand_poses = data[DeviceBase.TrackingTarget.HAND_RIGHT] left_wrist = left_hand_poses.get("wrist") right_wrist = right_hand_poses.get("wrist") @@ -117,6 +114,9 @@ def retarget(self, data: dict) -> torch.Tensor: # Combine all tensors into a single tensor return torch.cat([left_wrist_tensor, right_wrist_tensor, hand_joints_tensor]) + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.HAND_TRACKING] + def _retarget_abs(self, wrist: np.ndarray) -> np.ndarray: """Handle absolute pose retargeting. @@ -156,3 +156,13 @@ def _retarget_abs(self, wrist: np.ndarray) -> np.ndarray: usd_right_roll_link_in_world_quat = PoseUtils.quat_from_matrix(usd_right_roll_link_in_world_mat) return np.concatenate([usd_right_roll_link_in_world_pos, usd_right_roll_link_in_world_quat]) + + +@dataclass +class GR1T2RetargeterCfg(RetargeterCfg): + """Configuration for the GR1T2 retargeter.""" + + enable_visualization: bool = False + num_open_xr_hand_joints: int = 100 + hand_joint_names: list[str] | None = None # List of robot hand joint names + retargeter_type: type[RetargeterBase] = GR1T2Retargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/g1_lower_body_standing.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/g1_lower_body_standing.py index 9cf6ba09c426..1692b4a86d9b 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/g1_lower_body_standing.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/g1_lower_body_standing.py @@ -1,20 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch -from dataclasses import dataclass - -from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg +from __future__ import annotations +from dataclasses import dataclass -@dataclass -class G1LowerBodyStandingRetargeterCfg(RetargeterCfg): - """Configuration for the G1 lower body standing retargeter.""" +import torch - hip_height: float = 0.72 - """Height of the G1 robot hip in meters. The value is a fixed height suitable for G1 to do tabletop manipulation.""" +from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg class G1LowerBodyStandingRetargeter(RetargeterBase): @@ -22,7 +17,21 @@ class G1LowerBodyStandingRetargeter(RetargeterBase): def __init__(self, cfg: G1LowerBodyStandingRetargeterCfg): """Initialize the retargeter.""" + super().__init__(cfg) self.cfg = cfg def retarget(self, data: dict) -> torch.Tensor: return torch.tensor([0.0, 0.0, 0.0, self.cfg.hip_height], device=self.cfg.sim_device) + + def get_requirements(self) -> list[RetargeterBase.Requirement]: + # This retargeter does not consume any device data + return [] + + +@dataclass +class G1LowerBodyStandingRetargeterCfg(RetargeterCfg): + """Configuration for the G1 lower body standing retargeter.""" + + hip_height: float = 0.72 + """Height of the G1 robot hip in meters. The value is a fixed height suitable for G1 to do tabletop manipulation.""" + retargeter_type: type[RetargeterBase] = G1LowerBodyStandingRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/g1_motion_controller_locomotion.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/g1_motion_controller_locomotion.py new file mode 100644 index 000000000000..8acfe0abc027 --- /dev/null +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/g1_motion_controller_locomotion.py @@ -0,0 +1,87 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from dataclasses import dataclass + +import torch + +from isaaclab.devices.device_base import DeviceBase +from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg +from isaaclab.sim import SimulationContext + + +class G1LowerBodyStandingMotionControllerRetargeter(RetargeterBase): + """Provides lower body standing commands for the G1 robot.""" + + def __init__(self, cfg: G1LowerBodyStandingMotionControllerRetargeterCfg): + """Initialize the retargeter.""" + super().__init__(cfg) + self.cfg = cfg + self._hip_height = cfg.hip_height + + def retarget(self, data: dict) -> torch.Tensor: + left_thumbstick_x = 0.0 + left_thumbstick_y = 0.0 + right_thumbstick_x = 0.0 + right_thumbstick_y = 0.0 + + # Get controller data using enums + if ( + DeviceBase.TrackingTarget.CONTROLLER_LEFT in data + and data[DeviceBase.TrackingTarget.CONTROLLER_LEFT] is not None + ): + left_controller_data = data[DeviceBase.TrackingTarget.CONTROLLER_LEFT] + if len(left_controller_data) > DeviceBase.MotionControllerDataRowIndex.INPUTS.value: + left_inputs = left_controller_data[DeviceBase.MotionControllerDataRowIndex.INPUTS.value] + if len(left_inputs) > DeviceBase.MotionControllerInputIndex.THUMBSTICK_Y.value: + left_thumbstick_x = left_inputs[DeviceBase.MotionControllerInputIndex.THUMBSTICK_X.value] + left_thumbstick_y = left_inputs[DeviceBase.MotionControllerInputIndex.THUMBSTICK_Y.value] + + if ( + DeviceBase.TrackingTarget.CONTROLLER_RIGHT in data + and data[DeviceBase.TrackingTarget.CONTROLLER_RIGHT] is not None + ): + right_controller_data = data[DeviceBase.TrackingTarget.CONTROLLER_RIGHT] + if len(right_controller_data) > DeviceBase.MotionControllerDataRowIndex.INPUTS.value: + right_inputs = right_controller_data[DeviceBase.MotionControllerDataRowIndex.INPUTS.value] + if len(right_inputs) > DeviceBase.MotionControllerInputIndex.THUMBSTICK_Y.value: + right_thumbstick_x = right_inputs[DeviceBase.MotionControllerInputIndex.THUMBSTICK_X.value] + right_thumbstick_y = right_inputs[DeviceBase.MotionControllerInputIndex.THUMBSTICK_Y.value] + + # Thumbstick values are in the range of [-1, 1], so we need to scale them to the range of + # [-movement_scale, movement_scale] + left_thumbstick_x = left_thumbstick_x * self.cfg.movement_scale + left_thumbstick_y = left_thumbstick_y * self.cfg.movement_scale + + # Use rendering time step for deterministic hip height adjustment regardless of wall clock time. + dt = SimulationContext.instance().get_rendering_dt() + self._hip_height -= right_thumbstick_y * dt * self.cfg.rotation_scale + self._hip_height = max(0.4, min(1.0, self._hip_height)) + + return torch.tensor( + [-left_thumbstick_y, -left_thumbstick_x, -right_thumbstick_x, self._hip_height], + device=self.cfg.sim_device, + dtype=torch.float32, + ) + + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.MOTION_CONTROLLER] + + +@dataclass +class G1LowerBodyStandingMotionControllerRetargeterCfg(RetargeterCfg): + """Configuration for the G1 lower body standing retargeter.""" + + hip_height: float = 0.72 + """Height of the G1 robot hip in meters. The value is a fixed height suitable for G1 to do tabletop manipulation.""" + + movement_scale: float = 0.5 + """Scale the movement of the robot to the range of [-movement_scale, movement_scale].""" + + rotation_scale: float = 0.35 + """Scale the rotation of the robot to the range of [-rotation_scale, rotation_scale].""" + retargeter_type: type[RetargeterBase] = G1LowerBodyStandingMotionControllerRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_left_dexpilot.yml b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_left_dexpilot.yml index 476e20b1bc7b..de72352738fd 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_left_dexpilot.yml +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_left_dexpilot.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_right_dexpilot.yml b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_right_dexpilot.yml index c71cf4ed338a..5d0406da4365 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_right_dexpilot.yml +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/data/configs/dex-retargeting/unitree_hand_right_dexpilot.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_dex_retargeting_utils.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_dex_retargeting_utils.py index 802e73aca4a3..3d759003f854 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_dex_retargeting_utils.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_dex_retargeting_utils.py @@ -1,35 +1,42 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np +import logging import os + +import numpy as np import torch import yaml -from scipy.spatial.transform import Rotation as R - -import omni.log from dex_retargeting.retargeting_config import RetargetingConfig +from scipy.spatial.transform import Rotation as R from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path +# import logger +logger = logging.getLogger(__name__) + # The index to map the OpenXR hand joints to the hand joints used # in Dex-retargeting. _HAND_JOINTS_INDEX = [1, 2, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25] # The transformation matrices to convert hand pose to canonical view. -_OPERATOR2MANO_RIGHT = np.array([ - [0, -1, 0], - [-1, 0, 0], - [0, 0, -1], -]) - -_OPERATOR2MANO_LEFT = np.array([ - [0, -1, 0], - [-1, 0, 0], - [0, 0, -1], -]) +_OPERATOR2MANO_RIGHT = np.array( + [ + [0, -1, 0], + [-1, 0, 0], + [0, 0, -1], + ] +) + +_OPERATOR2MANO_LEFT = np.array( + [ + [0, -1, 0], + [-1, 0, 0], + [0, 0, -1], + ] +) _LEFT_HAND_JOINT_NAMES = [ "L_thumb_proximal_yaw_joint", @@ -74,8 +81,8 @@ def __init__( hand_joint_names: list[str], right_hand_config_filename: str = "unitree_hand_right_dexpilot.yml", left_hand_config_filename: str = "unitree_hand_left_dexpilot.yml", - left_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Mimic/G1_inspire_assets/retarget_inspire_white_left_hand.urdf", - right_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Mimic/G1_inspire_assets/retarget_inspire_white_right_hand.urdf", + left_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Mimic/G1_inspire_assets/retarget_inspire_white_left_hand.urdf", # noqa: E501 + right_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Mimic/G1_inspire_assets/retarget_inspire_white_right_hand.urdf", # noqa: E501 ): """Initialize the hand retargeting. @@ -107,7 +114,7 @@ def __init__( self.dof_names = self.left_dof_names + self.right_dof_names self.isaac_lab_hand_joint_names = hand_joint_names - omni.log.info("[UnitreeG1DexRetargeter] init done.") + logger.info("[UnitreeG1DexRetargeter] init done.") def _update_yaml_with_urdf_path(self, yaml_path: str, urdf_path: str): """Update YAML file with the correct URDF path. @@ -124,16 +131,16 @@ def _update_yaml_with_urdf_path(self, yaml_path: str, urdf_path: str): # Update the URDF path in the configuration if "retargeting" in config: config["retargeting"]["urdf_path"] = urdf_path - omni.log.info(f"Updated URDF path in {yaml_path} to {urdf_path}") + logger.info(f"Updated URDF path in {yaml_path} to {urdf_path}") else: - omni.log.warn(f"Unable to find 'retargeting' section in {yaml_path}") + logger.warning(f"Unable to find 'retargeting' section in {yaml_path}") # Write the updated configuration back to the file with open(yaml_path, "w") as file: yaml.dump(config, file) except Exception as e: - omni.log.error(f"Error updating YAML file {yaml_path}: {e}") + logger.error(f"Error updating YAML file {yaml_path}: {e}") def convert_hand_joints(self, hand_poses: dict[str, np.ndarray], operator2mano: np.ndarray) -> np.ndarray: """Prepares the hand joints data for retargeting. diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_upper_body_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_upper_body_retargeter.py index 98855cc352e1..17c73dc7ea40 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_upper_body_retargeter.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/inspire/g1_upper_body_retargeter.py @@ -1,38 +1,34 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + import contextlib +from dataclasses import dataclass + import numpy as np import torch -from dataclasses import dataclass import isaaclab.sim as sim_utils import isaaclab.utils.math as PoseUtils -from isaaclab.devices import OpenXRDevice +from isaaclab.devices.device_base import DeviceBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg -# This import exception is suppressed because g1_dex_retargeting_utils depends on pinocchio which is not available on windows +# This import exception is suppressed because g1_dex_retargeting_utils +# depends on pinocchio which is not available on Windows. with contextlib.suppress(Exception): from .g1_dex_retargeting_utils import UnitreeG1DexRetargeting -@dataclass -class UnitreeG1RetargeterCfg(RetargeterCfg): - """Configuration for the UnitreeG1 retargeter.""" - - enable_visualization: bool = False - num_open_xr_hand_joints: int = 100 - hand_joint_names: list[str] | None = None # List of robot hand joint names - - class UnitreeG1Retargeter(RetargeterBase): """Retargets OpenXR hand tracking data to GR1T2 hand end-effector commands. This retargeter maps hand tracking data from OpenXR to joint commands for the GR1T2 robot's hands. - It handles both left and right hands, converting poses of the hands in OpenXR format joint angles for the GR1T2 robot's hands. + It handles both left and right hands, converting poses of the hands in OpenXR format joint angles + for the GR1T2 robot's hands. """ def __init__( @@ -48,6 +44,7 @@ def __init__( hand_joint_names: List of robot hand joint names """ + super().__init__(cfg) self._hand_joint_names = cfg.hand_joint_names self._hands_controller = UnitreeG1DexRetargeting(self._hand_joint_names) @@ -81,8 +78,8 @@ def retarget(self, data: dict) -> torch.Tensor: """ # Access the left and right hand data using the enum key - left_hand_poses = data[OpenXRDevice.TrackingTarget.HAND_LEFT] - right_hand_poses = data[OpenXRDevice.TrackingTarget.HAND_RIGHT] + left_hand_poses = data[DeviceBase.TrackingTarget.HAND_LEFT] + right_hand_poses = data[DeviceBase.TrackingTarget.HAND_RIGHT] left_wrist = left_hand_poses.get("wrist") right_wrist = right_hand_poses.get("wrist") @@ -121,6 +118,9 @@ def retarget(self, data: dict) -> torch.Tensor: # Combine all tensors into a single tensor return torch.cat([left_wrist_tensor, right_wrist_tensor, hand_joints_tensor]) + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.HAND_TRACKING] + def _retarget_abs(self, wrist: np.ndarray, is_left: bool) -> np.ndarray: """Handle absolute pose retargeting. @@ -152,3 +152,13 @@ def _retarget_abs(self, wrist: np.ndarray, is_left: bool) -> np.ndarray: quat = PoseUtils.quat_from_matrix(rot_mat) return np.concatenate([pos.numpy(), quat.numpy()]) + + +@dataclass +class UnitreeG1RetargeterCfg(RetargeterCfg): + """Configuration for the UnitreeG1 retargeter.""" + + enable_visualization: bool = False + num_open_xr_hand_joints: int = 100 + hand_joint_names: list[str] | None = None # List of robot hand joint names + retargeter_type: type[RetargeterBase] = UnitreeG1Retargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_left_dexpilot.yml b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_left_dexpilot.yml index 282b5d8438bf..0f9f5416e1ca 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_left_dexpilot.yml +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_left_dexpilot.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_right_dexpilot.yml b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_right_dexpilot.yml index 2629f9354fa6..3908adcce0f6 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_right_dexpilot.yml +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/data/configs/dex-retargeting/g1_hand_right_dexpilot.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_dex_retargeting_utils.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_dex_retargeting_utils.py index 78d8ed667f92..6575eaaba41c 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_dex_retargeting_utils.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_dex_retargeting_utils.py @@ -1,20 +1,22 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 logging -import numpy as np import os + +import numpy as np import torch import yaml -from scipy.spatial.transform import Rotation as R - -import omni.log from dex_retargeting.retargeting_config import RetargetingConfig +from scipy.spatial.transform import Rotation as R from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path +# import logger +logger = logging.getLogger(__name__) + # yourdfpy loads visual/collision meshes with the hand URDFs; these aren't needed for # retargeting and clutter the logs, so we suppress them. logging.getLogger("dex_retargeting.yourdfpy").setLevel(logging.ERROR) @@ -24,17 +26,21 @@ _HAND_JOINTS_INDEX = [1, 2, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25] # The transformation matrices to convert hand pose to canonical view. -_OPERATOR2MANO_RIGHT = np.array([ - [0, 0, 1], - [1, 0, 0], - [0, 1, 0], -]) - -_OPERATOR2MANO_LEFT = np.array([ - [0, 0, 1], - [1, 0, 0], - [0, 1, 0], -]) +_OPERATOR2MANO_RIGHT = np.array( + [ + [0, 0, 1], + [1, 0, 0], + [0, 1, 0], + ] +) + +_OPERATOR2MANO_LEFT = np.array( + [ + [0, 0, 1], + [1, 0, 0], + [0, 1, 0], + ] +) # G1 robot hand joint names - 2 fingers and 1 thumb configuration _LEFT_HAND_JOINT_NAMES = [ @@ -69,8 +75,8 @@ def __init__( hand_joint_names: list[str], right_hand_config_filename: str = "g1_hand_right_dexpilot.yml", left_hand_config_filename: str = "g1_hand_left_dexpilot.yml", - left_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_dexpilot_asset/G1_left_hand.urdf", - right_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_dexpilot_asset/G1_right_hand.urdf", + left_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_dexpilot_asset/G1_left_hand.urdf", # noqa: E501 + right_hand_urdf_path: str = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_dexpilot_asset/G1_right_hand.urdf", # noqa: E501 ): """Initialize the hand retargeting. @@ -101,7 +107,7 @@ def __init__( self.dof_names = self.left_dof_names + self.right_dof_names self.isaac_lab_hand_joint_names = hand_joint_names - omni.log.info("[G1DexRetargeter] init done.") + logger.info("[G1DexRetargeter] init done.") def _update_yaml_with_urdf_path(self, yaml_path: str, urdf_path: str): """Update YAML file with the correct URDF path. @@ -118,16 +124,16 @@ def _update_yaml_with_urdf_path(self, yaml_path: str, urdf_path: str): # Update the URDF path in the configuration if "retargeting" in config: config["retargeting"]["urdf_path"] = urdf_path - omni.log.info(f"Updated URDF path in {yaml_path} to {urdf_path}") + logger.info(f"Updated URDF path in {yaml_path} to {urdf_path}") else: - omni.log.warn(f"Unable to find 'retargeting' section in {yaml_path}") + logger.warning(f"Unable to find 'retargeting' section in {yaml_path}") # Write the updated configuration back to the file with open(yaml_path, "w") as file: yaml.dump(config, file) except Exception as e: - omni.log.error(f"Error updating YAML file {yaml_path}: {e}") + logger.error(f"Error updating YAML file {yaml_path}: {e}") def convert_hand_joints(self, hand_poses: dict[str, np.ndarray], operator2mano: np.ndarray) -> np.ndarray: """Prepares the hand joints data for retargeting. diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_motion_ctrl_gripper.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_motion_ctrl_gripper.py new file mode 100644 index 000000000000..c22f40a283f3 --- /dev/null +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_motion_ctrl_gripper.py @@ -0,0 +1,154 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +import torch + +import isaaclab.utils.math as PoseUtils +from isaaclab.devices.device_base import DeviceBase +from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg + + +class G1TriHandUpperBodyMotionControllerGripperRetargeter(RetargeterBase): + """Retargeter for G1 gripper that outputs a boolean state based on controller trigger input, + concatenated with the retargeted wrist pose. + + Gripper: + - Uses hysteresis to prevent flickering when the trigger is near the threshold. + - Output is 0.0 for open, 1.0 for close. + + Wrist: + - Retargets absolute pose from controller to robot frame. + - Applies a fixed offset rotation for comfort/alignment. + """ + + def __init__(self, cfg: G1TriHandUpperBodyMotionControllerGripperRetargeterCfg): + """Initialize the retargeter. + + Args: + cfg: Configuration for the retargeter. + """ + super().__init__(cfg) + self._cfg = cfg + # Track previous state for hysteresis (left, right) + self._prev_left_state: float = 0.0 + self._prev_right_state: float = 0.0 + + def retarget(self, data: dict) -> torch.Tensor: + """Retarget controller inputs to gripper boolean state and wrist pose. + + Args: + data: Dictionary with MotionControllerTrackingTarget.LEFT/RIGHT keys + Each value is a 2D array: [pose(7), inputs(7)] + + Returns: + Tensor: [left_gripper_state(1), right_gripper_state(1), left_wrist(7), right_wrist(7)] + Wrist format: [x, y, z, qw, qx, qy, qz] + """ + # Get controller data + left_controller_data = data.get(DeviceBase.TrackingTarget.CONTROLLER_LEFT, np.array([])) + right_controller_data = data.get(DeviceBase.TrackingTarget.CONTROLLER_RIGHT, np.array([])) + + # --- Gripper Logic --- + # Extract hand state from controller data with hysteresis + left_hand_state: float = self._extract_hand_state(left_controller_data, self._prev_left_state) + right_hand_state: float = self._extract_hand_state(right_controller_data, self._prev_right_state) + + # Update previous states + self._prev_left_state = left_hand_state + self._prev_right_state = right_hand_state + + gripper_tensor = torch.tensor([left_hand_state, right_hand_state], dtype=torch.float32, device=self._sim_device) + + # --- Wrist Logic --- + # Default wrist poses (position + quaternion [w, x, y, z] as per default_wrist init) + # Note: default_wrist is [x, y, z, w, x, y, z] in reference, but seemingly used as [x,y,z, w,x,y,z] + default_wrist = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + # Extract poses from controller data + left_wrist = self._extract_wrist_pose(left_controller_data, default_wrist) + right_wrist = self._extract_wrist_pose(right_controller_data, default_wrist) + + # Convert to tensors + left_wrist_tensor = torch.tensor(self._retarget_abs(left_wrist), dtype=torch.float32, device=self._sim_device) + right_wrist_tensor = torch.tensor(self._retarget_abs(right_wrist), dtype=torch.float32, device=self._sim_device) + + # Concatenate: [gripper(2), left_wrist(7), right_wrist(7)] + return torch.cat([gripper_tensor, left_wrist_tensor, right_wrist_tensor]) + + def _extract_hand_state(self, controller_data: np.ndarray, prev_state: float) -> float: + """Extract hand state from controller data with hysteresis. + + Args: + controller_data: 2D array [pose(7), inputs(7)] + prev_state: Previous hand state (0.0 or 1.0) + + Returns: + Hand state as float (0.0 for open, 1.0 for close) + """ + if len(controller_data) <= DeviceBase.MotionControllerDataRowIndex.INPUTS.value: + return 0.0 + + # Extract inputs from second row + inputs = controller_data[DeviceBase.MotionControllerDataRowIndex.INPUTS.value] + if len(inputs) < len(DeviceBase.MotionControllerInputIndex): + return 0.0 + + # Extract specific inputs using enum + trigger = inputs[DeviceBase.MotionControllerInputIndex.TRIGGER.value] # 0.0 to 1.0 (analog) + + # Apply hysteresis + if prev_state < 0.5: # Currently open + return 1.0 if trigger > self._cfg.threshold_high else 0.0 + else: # Currently closed + return 0.0 if trigger < self._cfg.threshold_low else 1.0 + + def _extract_wrist_pose(self, controller_data: np.ndarray, default_pose: np.ndarray) -> np.ndarray: + """Extract wrist pose from controller data. + + Args: + controller_data: 2D array [pose(7), inputs(7)] + default_pose: Default pose to use if no data + + Returns: + Wrist pose array [x, y, z, w, x, y, z] + """ + if len(controller_data) > DeviceBase.MotionControllerDataRowIndex.POSE.value: + return controller_data[DeviceBase.MotionControllerDataRowIndex.POSE.value] + return default_pose + + def _retarget_abs(self, wrist: np.ndarray) -> np.ndarray: + """Handle absolute pose retargeting for controller wrists.""" + wrist_pos = torch.tensor(wrist[:3], dtype=torch.float32) + wrist_quat = torch.tensor(wrist[3:], dtype=torch.float32) + + # Combined -75° (rather than -90° for wrist comfort) Y rotation + 90° Z rotation + # This is equivalent to (0, -75, 90) in euler angles + combined_quat = torch.tensor([0.5358, -0.4619, 0.5358, 0.4619], dtype=torch.float32) + + openxr_pose = PoseUtils.make_pose(wrist_pos, PoseUtils.matrix_from_quat(wrist_quat)) + transform_pose = PoseUtils.make_pose(torch.zeros(3), PoseUtils.matrix_from_quat(combined_quat)) + + result_pose = PoseUtils.pose_in_A_to_pose_in_B(transform_pose, openxr_pose) + pos, rot_mat = PoseUtils.unmake_pose(result_pose) + quat = PoseUtils.quat_from_matrix(rot_mat) + + return np.concatenate([pos.numpy(), quat.numpy()]) + + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.MOTION_CONTROLLER] + + +@dataclass +class G1TriHandUpperBodyMotionControllerGripperRetargeterCfg(RetargeterCfg): + """Configuration for the G1 boolean gripper and wrist retargeter.""" + + threshold_high: float = 0.6 # Threshold to close hand + threshold_low: float = 0.4 # Threshold to open hand + retargeter_type: type[RetargeterBase] = G1TriHandUpperBodyMotionControllerGripperRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_motion_ctrl_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_motion_ctrl_retargeter.py new file mode 100644 index 000000000000..0138bdf6d6b9 --- /dev/null +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_motion_ctrl_retargeter.py @@ -0,0 +1,230 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +import torch + +import isaaclab.sim as sim_utils +import isaaclab.utils.math as PoseUtils +from isaaclab.devices.device_base import DeviceBase +from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg +from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg + + +class G1TriHandUpperBodyMotionControllerRetargeter(RetargeterBase): + """Simple retargeter that maps motion controller inputs to G1 hand joints. + + Mapping: + - A button (digital 0/1) → Thumb joints + - Trigger (analog 0-1) → Index finger joints + - Squeeze (analog 0-1) → Middle finger joints + """ + + def __init__(self, cfg: G1TriHandUpperBodyMotionControllerRetargeterCfg): + """Initialize the retargeter.""" + super().__init__(cfg) + self._sim_device = cfg.sim_device + self._hand_joint_names = cfg.hand_joint_names + self._enable_visualization = cfg.enable_visualization + + if cfg.hand_joint_names is None: + raise ValueError("hand_joint_names must be provided") + + # Initialize visualization if enabled + if self._enable_visualization: + marker_cfg = VisualizationMarkersCfg( + prim_path="/Visuals/g1_controller_markers", + markers={ + "joint": sim_utils.SphereCfg( + radius=0.01, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), + ), + }, + ) + self._markers = VisualizationMarkers(marker_cfg) + + def retarget(self, data: dict) -> torch.Tensor: + """Convert controller inputs to robot commands. + + Args: + data: Dictionary with MotionControllerTrackingTarget.LEFT/RIGHT keys + Each value is a 2D array: [pose(7), inputs(7)] + + Returns: + Tensor: [left_wrist(7), right_wrist(7), hand_joints(14)] + hand_joints order: + [ + left_proximal(3), right_proximal(3), left_distal(2), left_thumb_middle(1), + right_distal(2), right_thumb_middle(1), left_thumb_tip(1), right_thumb_tip(1) + ] + """ + + # Get controller data + left_controller_data = data.get(DeviceBase.TrackingTarget.CONTROLLER_LEFT, np.array([])) + right_controller_data = data.get(DeviceBase.TrackingTarget.CONTROLLER_RIGHT, np.array([])) + + # Default wrist poses (position + quaternion) + default_wrist = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + # Extract poses from controller data + left_wrist = self._extract_wrist_pose(left_controller_data, default_wrist) + right_wrist = self._extract_wrist_pose(right_controller_data, default_wrist) + + # Map controller inputs to hand joints + left_hand_joints = self._map_to_hand_joints(left_controller_data, is_left=True) + right_hand_joints = self._map_to_hand_joints(right_controller_data, is_left=False) + + # Negate left hand joints for proper mirroring + left_hand_joints = -left_hand_joints + + # Combine joints in the expected order: + # [left_proximal(3), right_proximal(3), left_distal(2), left_thumb_middle(1), + # right_distal(2), right_thumb_middle(1), left_thumb_tip(1), right_thumb_tip(1)] + all_hand_joints = np.array( + [ + left_hand_joints[3], # left_index_proximal + left_hand_joints[5], # left_middle_proximal + left_hand_joints[0], # left_thumb_base + right_hand_joints[3], # right_index_proximal + right_hand_joints[5], # right_middle_proximal + right_hand_joints[0], # right_thumb_base + left_hand_joints[4], # left_index_distal + left_hand_joints[6], # left_middle_distal + left_hand_joints[1], # left_thumb_middle + right_hand_joints[4], # right_index_distal + right_hand_joints[6], # right_middle_distal + right_hand_joints[1], # right_thumb_middle + left_hand_joints[2], # left_thumb_tip + right_hand_joints[2], # right_thumb_tip + ] + ) + + # Convert to tensors + left_wrist_tensor = torch.tensor( + self._retarget_abs(left_wrist, is_left=True), dtype=torch.float32, device=self._sim_device + ) + right_wrist_tensor = torch.tensor( + self._retarget_abs(right_wrist, is_left=False), dtype=torch.float32, device=self._sim_device + ) + hand_joints_tensor = torch.tensor(all_hand_joints, dtype=torch.float32, device=self._sim_device) + + return torch.cat([left_wrist_tensor, right_wrist_tensor, hand_joints_tensor]) + + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.MOTION_CONTROLLER] + + def _extract_wrist_pose(self, controller_data: np.ndarray, default_pose: np.ndarray) -> np.ndarray: + """Extract wrist pose from controller data. + + Args: + controller_data: 2D array [pose(7), inputs(7)] + default_pose: Default pose to use if no data + + Returns: + Wrist pose array [x, y, z, w, x, y, z] + """ + if len(controller_data) > DeviceBase.MotionControllerDataRowIndex.POSE.value: + return controller_data[DeviceBase.MotionControllerDataRowIndex.POSE.value] + return default_pose + + def _map_to_hand_joints(self, controller_data: np.ndarray, is_left: bool) -> np.ndarray: + """Map controller inputs to hand joint angles. + + Args: + controller_data: 2D array [pose(7), inputs(7)] + is_left: True for left hand, False for right hand + + Returns: + Hand joint angles (7 joints per hand) in radians + """ + + # Initialize all joints to zero + hand_joints = np.zeros(7) + + if len(controller_data) <= DeviceBase.MotionControllerDataRowIndex.INPUTS.value: + return hand_joints + + # Extract inputs from second row + inputs = controller_data[DeviceBase.MotionControllerDataRowIndex.INPUTS.value] + + if len(inputs) < len(DeviceBase.MotionControllerInputIndex): + return hand_joints + + # Extract specific inputs using enum + trigger = inputs[DeviceBase.MotionControllerInputIndex.TRIGGER.value] # 0.0 to 1.0 (analog) + squeeze = inputs[DeviceBase.MotionControllerInputIndex.SQUEEZE.value] # 0.0 to 1.0 (analog) + + # Grasping logic: + # If trigger is pressed, we grasp with index and thumb. + # If squeeze is pressed, we grasp with middle and thumb. + # If both are pressed, we grasp with index, middle, and thumb. + # The thumb rotates towards the direction of the pressing finger. + # If both are pressed, the thumb stays in the middle. + + thumb_button = max(trigger, squeeze) + + # Map to G1 hand joints (in radians) + # Thumb joints (3 joints) - controlled by A button (digital) + thumb_angle = -thumb_button # Max 1 radian ≈ 57° + + # Thumb rotation: + # If trigger is pressed, we rotate the thumb toward the index finger. + # If squeeze is pressed, we rotate the thumb toward the middle finger. + # If both are pressed, the thumb stays between the index and middle fingers. + # Trigger pushes toward +0.5, squeeze pushes toward -0.5 + # trigger=1,squeeze=0 → 0.5; trigger=0,squeeze=1 → -0.5; both=1 → 0 + thumb_rotation = 0.5 * trigger - 0.5 * squeeze + + if not is_left: + thumb_rotation = -thumb_rotation + + # These values were found empirically to get a good gripper pose. + + hand_joints[0] = thumb_rotation # thumb_0_joint (base) + hand_joints[1] = thumb_angle * 0.4 # thumb_1_joint (middle) + hand_joints[2] = thumb_angle * 0.7 # thumb_2_joint (tip) + + # Index finger joints (2 joints) - controlled by trigger (analog) + index_angle = trigger * 1.0 # Max 1.0 radians ≈ 57° + hand_joints[3] = index_angle # index_0_joint (proximal) + hand_joints[4] = index_angle # index_1_joint (distal) + + # Middle finger joints (2 joints) - controlled by squeeze (analog) + middle_angle = squeeze * 1.0 # Max 1.0 radians ≈ 57° + hand_joints[5] = middle_angle # middle_0_joint (proximal) + hand_joints[6] = middle_angle # middle_1_joint (distal) + + return hand_joints + + def _retarget_abs(self, wrist: np.ndarray, is_left: bool) -> np.ndarray: + """Handle absolute pose retargeting for controller wrists.""" + wrist_pos = torch.tensor(wrist[:3], dtype=torch.float32) + wrist_quat = torch.tensor(wrist[3:], dtype=torch.float32) + + # Combined -75° (rather than -90° for wrist comfort) Y rotation + 90° Z rotation + # This is equivalent to (0, -75, 90) in euler angles + combined_quat = torch.tensor([0.5358, -0.4619, 0.5358, 0.4619], dtype=torch.float32) + + openxr_pose = PoseUtils.make_pose(wrist_pos, PoseUtils.matrix_from_quat(wrist_quat)) + transform_pose = PoseUtils.make_pose(torch.zeros(3), PoseUtils.matrix_from_quat(combined_quat)) + + result_pose = PoseUtils.pose_in_A_to_pose_in_B(transform_pose, openxr_pose) + pos, rot_mat = PoseUtils.unmake_pose(result_pose) + quat = PoseUtils.quat_from_matrix(rot_mat) + + return np.concatenate([pos.numpy(), quat.numpy()]) + + +@dataclass +class G1TriHandUpperBodyMotionControllerRetargeterCfg(RetargeterCfg): + """Configuration for the G1 Controller Upper Body retargeter.""" + + enable_visualization: bool = False + hand_joint_names: list[str] | None = None # List of robot hand joint names + retargeter_type: type[RetargeterBase] = G1TriHandUpperBodyMotionControllerRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_retargeter.py index 41f7f49fd9f8..9c8651f43de1 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_retargeter.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/humanoid/unitree/trihand/g1_upper_body_retargeter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,30 +6,23 @@ from __future__ import annotations import contextlib +from dataclasses import dataclass + import numpy as np import torch -from dataclasses import dataclass import isaaclab.sim as sim_utils import isaaclab.utils.math as PoseUtils -from isaaclab.devices import OpenXRDevice +from isaaclab.devices.device_base import DeviceBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg -# This import exception is suppressed because g1_dex_retargeting_utils depends on pinocchio which is not available on windows +# This import exception is suppressed because g1_dex_retargeting_utils depends +# on pinocchio which is not available on Windows. with contextlib.suppress(Exception): from .g1_dex_retargeting_utils import G1TriHandDexRetargeting -@dataclass -class G1TriHandUpperBodyRetargeterCfg(RetargeterCfg): - """Configuration for the G1UpperBody retargeter.""" - - enable_visualization: bool = False - num_open_xr_hand_joints: int = 100 - hand_joint_names: list[str] | None = None # List of robot hand joint names - - class G1TriHandUpperBodyRetargeter(RetargeterBase): """Retargets OpenXR data to G1 upper body commands. @@ -47,6 +40,7 @@ def __init__( Args: cfg: Configuration for the retargeter. """ + super().__init__(cfg) # Store device name for runtime retrieval self._sim_device = cfg.sim_device @@ -87,8 +81,8 @@ def retarget(self, data: dict) -> torch.Tensor: """ # Access the left and right hand data using the enum key - left_hand_poses = data[OpenXRDevice.TrackingTarget.HAND_LEFT] - right_hand_poses = data[OpenXRDevice.TrackingTarget.HAND_RIGHT] + left_hand_poses = data[DeviceBase.TrackingTarget.HAND_LEFT] + right_hand_poses = data[DeviceBase.TrackingTarget.HAND_RIGHT] left_wrist = left_hand_poses.get("wrist") right_wrist = right_hand_poses.get("wrist") @@ -136,6 +130,9 @@ def retarget(self, data: dict) -> torch.Tensor: # Combine all tensors into a single tensor return torch.cat([left_wrist_tensor, right_wrist_tensor, hand_joints_tensor]) + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.HAND_TRACKING] + def _retarget_abs(self, wrist: np.ndarray, is_left: bool) -> np.ndarray: """Handle absolute pose retargeting. @@ -164,3 +161,13 @@ def _retarget_abs(self, wrist: np.ndarray, is_left: bool) -> np.ndarray: quat = PoseUtils.quat_from_matrix(rot_mat) return np.concatenate([pos.numpy(), quat.numpy()]) + + +@dataclass +class G1TriHandUpperBodyRetargeterCfg(RetargeterCfg): + """Configuration for the G1 Controller Upper Body retargeter.""" + + enable_visualization: bool = False + num_open_xr_hand_joints: int = 100 + hand_joint_names: list[str] | None = None # List of robot hand joint names + retargeter_type: type[RetargeterBase] = G1TriHandUpperBodyRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/__init__.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/__init__.py index 819dfac07909..426b8ac1002c 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/__init__.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/gripper_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/gripper_retargeter.py index 2174e148d447..9ae2031b4d81 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/gripper_retargeter.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/gripper_retargeter.py @@ -1,21 +1,17 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np -import torch +from __future__ import annotations + from dataclasses import dataclass from typing import Final -from isaaclab.devices import OpenXRDevice -from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg - - -@dataclass -class GripperRetargeterCfg(RetargeterCfg): - """Configuration for gripper retargeter.""" +import numpy as np +import torch - bound_hand: OpenXRDevice.TrackingTarget = OpenXRDevice.TrackingTarget.HAND_RIGHT +from isaaclab.devices.device_base import DeviceBase +from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg class GripperRetargeter(RetargeterBase): @@ -41,10 +37,9 @@ def __init__( super().__init__(cfg) """Initialize the gripper retargeter.""" # Store the hand to track - if cfg.bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]: + if cfg.bound_hand not in [DeviceBase.TrackingTarget.HAND_LEFT, DeviceBase.TrackingTarget.HAND_RIGHT]: raise ValueError( - "bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or" - " OpenXRDevice.TrackingTarget.HAND_RIGHT" + "bound_hand must be either DeviceBase.TrackingTarget.HAND_LEFT or DeviceBase.TrackingTarget.HAND_RIGHT" ) self.bound_hand = cfg.bound_hand # Initialize gripper state @@ -71,6 +66,9 @@ def retarget(self, data: dict) -> torch.Tensor: return torch.tensor([gripper_value], dtype=torch.float32, device=self._sim_device) + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.HAND_TRACKING] + def _calculate_gripper_command(self, thumb_pos: np.ndarray, index_pos: np.ndarray) -> bool: """Calculate gripper command from finger positions with hysteresis. @@ -90,3 +88,11 @@ def _calculate_gripper_command(self, thumb_pos: np.ndarray, index_pos: np.ndarra self._previous_gripper_command = True return self._previous_gripper_command + + +@dataclass +class GripperRetargeterCfg(RetargeterCfg): + """Configuration for gripper retargeter.""" + + bound_hand: DeviceBase.TrackingTarget = DeviceBase.TrackingTarget.HAND_RIGHT + retargeter_type: type[RetargeterBase] = GripperRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_abs_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_abs_retargeter.py index 789ff3c44f6c..d69af88cfcce 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_abs_retargeter.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_abs_retargeter.py @@ -1,29 +1,21 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + +from dataclasses import dataclass + import numpy as np import torch -from dataclasses import dataclass from scipy.spatial.transform import Rotation, Slerp -from isaaclab.devices import OpenXRDevice +from isaaclab.devices.device_base import DeviceBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg from isaaclab.markers import VisualizationMarkers from isaaclab.markers.config import FRAME_MARKER_CFG -@dataclass -class Se3AbsRetargeterCfg(RetargeterCfg): - """Configuration for absolute position retargeter.""" - - zero_out_xy_rotation: bool = True - use_wrist_rotation: bool = False - use_wrist_position: bool = True - enable_visualization: bool = False - bound_hand: OpenXRDevice.TrackingTarget = OpenXRDevice.TrackingTarget.HAND_RIGHT - - class Se3AbsRetargeter(RetargeterBase): """Retargets OpenXR hand tracking data to end-effector commands using absolute positioning. @@ -44,7 +36,7 @@ def __init__( """Initialize the retargeter. Args: - bound_hand: The hand to track (OpenXRDevice.TrackingTarget.HAND_LEFT or OpenXRDevice.TrackingTarget.HAND_RIGHT) + bound_hand: The hand to track (DeviceBase.TrackingTarget.HAND_LEFT or DeviceBase.TrackingTarget.HAND_RIGHT) zero_out_xy_rotation: If True, zero out rotation around x and y axes use_wrist_rotation: If True, use wrist rotation instead of finger average use_wrist_position: If True, use wrist position instead of pinch position @@ -52,10 +44,9 @@ def __init__( device: The device to place the returned tensor on ('cpu' or 'cuda') """ super().__init__(cfg) - if cfg.bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]: + if cfg.bound_hand not in [DeviceBase.TrackingTarget.HAND_LEFT, DeviceBase.TrackingTarget.HAND_RIGHT]: raise ValueError( - "bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or" - " OpenXRDevice.TrackingTarget.HAND_RIGHT" + "bound_hand must be either DeviceBase.TrackingTarget.HAND_LEFT or DeviceBase.TrackingTarget.HAND_RIGHT" ) self.bound_hand = cfg.bound_hand @@ -97,6 +88,9 @@ def retarget(self, data: dict) -> torch.Tensor: return ee_command + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.HAND_TRACKING] + def _retarget_abs(self, thumb_tip: np.ndarray, index_tip: np.ndarray, wrist: np.ndarray) -> np.ndarray: """Handle absolute pose retargeting. @@ -164,3 +158,15 @@ def _update_visualization(self): quat = Rotation.from_matrix(self._visualization_rot).as_quat() rot = np.array([np.array([quat[3], quat[0], quat[1], quat[2]])]) self._goal_marker.visualize(translations=trans, orientations=rot) + + +@dataclass +class Se3AbsRetargeterCfg(RetargeterCfg): + """Configuration for absolute position retargeter.""" + + zero_out_xy_rotation: bool = True + use_wrist_rotation: bool = False + use_wrist_position: bool = True + enable_visualization: bool = False + bound_hand: DeviceBase.TrackingTarget = DeviceBase.TrackingTarget.HAND_RIGHT + retargeter_type: type[RetargeterBase] = Se3AbsRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_rel_retargeter.py b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_rel_retargeter.py index 1a3d80ec2494..360b1c29c347 100644 --- a/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_rel_retargeter.py +++ b/source/isaaclab/isaaclab/devices/openxr/retargeters/manipulator/se3_rel_retargeter.py @@ -1,33 +1,21 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + +from dataclasses import dataclass + import numpy as np import torch -from dataclasses import dataclass from scipy.spatial.transform import Rotation -from isaaclab.devices import OpenXRDevice +from isaaclab.devices.device_base import DeviceBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg from isaaclab.markers import VisualizationMarkers from isaaclab.markers.config import FRAME_MARKER_CFG -@dataclass -class Se3RelRetargeterCfg(RetargeterCfg): - """Configuration for relative position retargeter.""" - - zero_out_xy_rotation: bool = True - use_wrist_rotation: bool = False - use_wrist_position: bool = True - delta_pos_scale_factor: float = 10.0 - delta_rot_scale_factor: float = 10.0 - alpha_pos: float = 0.5 - alpha_rot: float = 0.5 - enable_visualization: bool = False - bound_hand: OpenXRDevice.TrackingTarget = OpenXRDevice.TrackingTarget.HAND_RIGHT - - class Se3RelRetargeter(RetargeterBase): """Retargets OpenXR hand tracking data to end-effector commands using relative positioning. @@ -49,22 +37,23 @@ def __init__( """Initialize the relative motion retargeter. Args: - bound_hand: The hand to track (OpenXRDevice.TrackingTarget.HAND_LEFT or OpenXRDevice.TrackingTarget.HAND_RIGHT) + bound_hand: The hand to track (DeviceBase.TrackingTarget.HAND_LEFT or DeviceBase.TrackingTarget.HAND_RIGHT) zero_out_xy_rotation: If True, ignore rotations around x and y axes, allowing only z-axis rotation use_wrist_rotation: If True, use wrist rotation for control instead of averaging finger orientations use_wrist_position: If True, use wrist position instead of pinch position (midpoint between fingers) delta_pos_scale_factor: Amplification factor for position changes (higher = larger robot movements) delta_rot_scale_factor: Amplification factor for rotation changes (higher = larger robot rotations) - alpha_pos: Position smoothing parameter (0-1); higher values track more closely to input, lower values smooth more - alpha_rot: Rotation smoothing parameter (0-1); higher values track more closely to input, lower values smooth more + alpha_pos: Position smoothing parameter (0-1); higher values track more closely to input, + lower values smooth more + alpha_rot: Rotation smoothing parameter (0-1); higher values track more closely to input, + lower values smooth more enable_visualization: If True, show a visual marker representing the target end-effector pose device: The device to place the returned tensor on ('cpu' or 'cuda') """ # Store the hand to track - if cfg.bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]: + if cfg.bound_hand not in [DeviceBase.TrackingTarget.HAND_LEFT, DeviceBase.TrackingTarget.HAND_RIGHT]: raise ValueError( - "bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or" - " OpenXRDevice.TrackingTarget.HAND_RIGHT" + "bound_hand must be either DeviceBase.TrackingTarget.HAND_LEFT or DeviceBase.TrackingTarget.HAND_RIGHT" ) super().__init__(cfg) self.bound_hand = cfg.bound_hand @@ -130,6 +119,9 @@ def retarget(self, data: dict) -> torch.Tensor: return ee_command + def get_requirements(self) -> list[RetargeterBase.Requirement]: + return [RetargeterBase.Requirement.HAND_TRACKING] + def _calculate_delta_pose(self, joint_pose: np.ndarray, previous_joint_pose: np.ndarray) -> np.ndarray: """Calculate delta pose from previous joint pose. @@ -206,3 +198,19 @@ def _update_visualization(self): quat = Rotation.from_matrix(self._visualization_rot).as_quat() rot = np.array([np.array([quat[3], quat[0], quat[1], quat[2]])]) self._goal_marker.visualize(translations=trans, orientations=rot) + + +@dataclass +class Se3RelRetargeterCfg(RetargeterCfg): + """Configuration for relative position retargeter.""" + + zero_out_xy_rotation: bool = True + use_wrist_rotation: bool = False + use_wrist_position: bool = True + delta_pos_scale_factor: float = 10.0 + delta_rot_scale_factor: float = 10.0 + alpha_pos: float = 0.5 + alpha_rot: float = 0.5 + enable_visualization: bool = False + bound_hand: DeviceBase.TrackingTarget = DeviceBase.TrackingTarget.HAND_RIGHT + retargeter_type: type[RetargeterBase] = Se3RelRetargeter diff --git a/source/isaaclab/isaaclab/devices/openxr/xr_anchor_utils.py b/source/isaaclab/isaaclab/devices/openxr/xr_anchor_utils.py new file mode 100644 index 000000000000..195d94c0dc15 --- /dev/null +++ b/source/isaaclab/isaaclab/devices/openxr/xr_anchor_utils.py @@ -0,0 +1,176 @@ +# 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 + +# Copyright (c) 2022-2025, The Isaac Lab Project Developers. +# SPDX-License-Identifier: BSD-3-Clause +"""Utilities for synchronizing XR anchor pose with a reference prim and XR config.""" + +from __future__ import annotations + +import contextlib +import logging +import math +from typing import Any + +import numpy as np + +# import logger +logger = logging.getLogger(__name__) + +from isaaclab.sim import SimulationContext +from isaaclab.sim.utils.stage import get_current_stage_id + +from .xr_cfg import XrAnchorRotationMode + +with contextlib.suppress(ModuleNotFoundError): + import usdrt + from pxr import Gf as pxrGf + from usdrt import Rt + + +class XrAnchorSynchronizer: + """Keeps the XR anchor prim aligned with a reference prim according to XR config.""" + + def __init__(self, xr_core: Any, xr_cfg: Any, xr_anchor_headset_path: str): + self._xr_core = xr_core + self._xr_cfg = xr_cfg + self._xr_anchor_headset_path = xr_anchor_headset_path + + self.__anchor_prim_initial_quat = None + self.__anchor_prim_initial_height = None + self.__smoothed_anchor_quat = None + self.__last_anchor_quat = None + self.__anchor_rotation_enabled = True + + # Resolve USD layer identifier of the anchor for updates + try: + from isaacsim.core.utils.stage import get_current_stage + + stage = get_current_stage() + xr_anchor_headset_prim = stage.GetPrimAtPath(self._xr_anchor_headset_path) + prim_stack = xr_anchor_headset_prim.GetPrimStack() if xr_anchor_headset_prim is not None else None + self.__anchor_headset_layer_identifier = prim_stack[0].layer.identifier if prim_stack else None + except Exception: + self.__anchor_headset_layer_identifier = None + + def reset(self): + self.__anchor_prim_initial_quat = None + self.__anchor_prim_initial_height = None + self.__smoothed_anchor_quat = None + self.__last_anchor_quat = None + self.__anchor_rotation_enabled = True + self.sync_headset_to_anchor() + + def toggle_anchor_rotation(self): + self.__anchor_rotation_enabled = not self.__anchor_rotation_enabled + logger.info(f"XR: Toggling anchor rotation: {self.__anchor_rotation_enabled}") + + def sync_headset_to_anchor(self): + """Sync XR anchor pose in USD from reference prim (in Fabric/usdrt).""" + try: + if self._xr_cfg.anchor_prim_path is None: + return + + stage_id = get_current_stage_id() + rt_stage = usdrt.Usd.Stage.Attach(stage_id) + if rt_stage is None: + return + + rt_prim = rt_stage.GetPrimAtPath(self._xr_cfg.anchor_prim_path) + if rt_prim is None: + return + + rt_xformable = Rt.Xformable(rt_prim) + if rt_xformable is None: + return + + world_matrix_attr = rt_xformable.GetFabricHierarchyWorldMatrixAttr() + if world_matrix_attr is None: + return + + rt_matrix = world_matrix_attr.Get() + rt_pos = rt_matrix.ExtractTranslation() + + if self.__anchor_prim_initial_quat is None: + self.__anchor_prim_initial_quat = rt_matrix.ExtractRotationQuat() + + if getattr(self._xr_cfg, "fixed_anchor_height", False): + if self.__anchor_prim_initial_height is None: + self.__anchor_prim_initial_height = rt_pos[2] + rt_pos[2] = self.__anchor_prim_initial_height + + pxr_anchor_pos = pxrGf.Vec3d(*rt_pos) + pxrGf.Vec3d(*self._xr_cfg.anchor_pos) + + w, x, y, z = self._xr_cfg.anchor_rot + pxr_cfg_quat = pxrGf.Quatd(w, pxrGf.Vec3d(x, y, z)) + + pxr_anchor_quat = pxr_cfg_quat + + if self._xr_cfg.anchor_rotation_mode in ( + XrAnchorRotationMode.FOLLOW_PRIM, + XrAnchorRotationMode.FOLLOW_PRIM_SMOOTHED, + ): + rt_prim_quat = rt_matrix.ExtractRotationQuat() + rt_delta_quat = rt_prim_quat * self.__anchor_prim_initial_quat.GetInverse() + pxr_delta_quat = pxrGf.Quatd(rt_delta_quat.GetReal(), pxrGf.Vec3d(*rt_delta_quat.GetImaginary())) + + # yaw-only about Z (right-handed, Z-up) + wq = pxr_delta_quat.GetReal() + ix, iy, iz = pxr_delta_quat.GetImaginary() + yaw = math.atan2(2.0 * (wq * iz + ix * iy), 1.0 - 2.0 * (iy * iy + iz * iz)) + cy = math.cos(yaw * 0.5) + sy = math.sin(yaw * 0.5) + pxr_delta_yaw_only_quat = pxrGf.Quatd(cy, pxrGf.Vec3d(0.0, 0.0, sy)) + pxr_anchor_quat = pxr_delta_yaw_only_quat * pxr_cfg_quat + + if self._xr_cfg.anchor_rotation_mode == XrAnchorRotationMode.FOLLOW_PRIM_SMOOTHED: + if self.__smoothed_anchor_quat is None: + self.__smoothed_anchor_quat = pxr_anchor_quat + else: + dt = SimulationContext.instance().get_rendering_dt() + alpha = 1.0 - math.exp(-dt / max(self._xr_cfg.anchor_rotation_smoothing_time, 1e-6)) + alpha = min(1.0, max(0.05, alpha)) + self.__smoothed_anchor_quat = pxrGf.Slerp(alpha, self.__smoothed_anchor_quat, pxr_anchor_quat) + pxr_anchor_quat = self.__smoothed_anchor_quat + + elif self._xr_cfg.anchor_rotation_mode == XrAnchorRotationMode.CUSTOM: + if self._xr_cfg.anchor_rotation_custom_func is not None: + rt_prim_quat = rt_matrix.ExtractRotationQuat() + anchor_prim_pose = np.array( + [ + rt_pos[0], + rt_pos[1], + rt_pos[2], + rt_prim_quat.GetReal(), + rt_prim_quat.GetImaginary()[0], + rt_prim_quat.GetImaginary()[1], + rt_prim_quat.GetImaginary()[2], + ], + dtype=np.float64, + ) + # Previous headpose must be provided by caller; fall back to zeros. + prev_head = getattr(self, "_previous_headpose", np.zeros(7, dtype=np.float64)) + np_array_quat = self._xr_cfg.anchor_rotation_custom_func(prev_head, anchor_prim_pose) + w, x, y, z = np_array_quat + pxr_anchor_quat = pxrGf.Quatd(w, pxrGf.Vec3d(x, y, z)) + + pxr_mat = pxrGf.Matrix4d() + pxr_mat.SetTranslateOnly(pxr_anchor_pos) + + if self.__anchor_rotation_enabled: + pxr_mat.SetRotateOnly(pxr_anchor_quat) + self.__last_anchor_quat = pxr_anchor_quat + else: + if self.__last_anchor_quat is None: + self.__last_anchor_quat = pxr_anchor_quat + + pxr_mat.SetRotateOnly(self.__last_anchor_quat) + self.__smoothed_anchor_quat = self.__last_anchor_quat + + self._xr_core.set_world_transform_matrix( + self._xr_anchor_headset_path, pxr_mat, self.__anchor_headset_layer_identifier + ) + except Exception as e: + logger.warning(f"XR: Anchor sync failed: {e}") diff --git a/source/isaaclab/isaaclab/devices/openxr/xr_cfg.py b/source/isaaclab/isaaclab/devices/openxr/xr_cfg.py index 41e13078eb55..1eaee292eaee 100644 --- a/source/isaaclab/isaaclab/devices/openxr/xr_cfg.py +++ b/source/isaaclab/isaaclab/devices/openxr/xr_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +8,30 @@ from __future__ import annotations +import enum +from collections.abc import Callable + +import numpy as np + from isaaclab.utils import configclass +class XrAnchorRotationMode(enum.Enum): + """Enumeration for XR anchor rotation modes.""" + + FIXED = "fixed" + """Fixed rotation mode: sets rotation once and doesn't change it.""" + + FOLLOW_PRIM = "follow_prim" + """Follow prim rotation mode: rotation follows prim's rotation.""" + + FOLLOW_PRIM_SMOOTHED = "follow_prim_smoothed" + """Follow prim rotation mode with smooth interpolation: rotation smoothly follows prim's rotation using slerp.""" + + CUSTOM = "custom_rotation" + """Custom rotation mode: user provided function to calculate the rotation.""" + + @configclass class XrCfg: """Configuration for viewing and interacting with the environment through an XR device.""" @@ -30,12 +51,60 @@ class XrCfg: This quantity is only effective if :attr:`xr_anchor_pos` is set. """ + anchor_prim_path: str | None = None + """Specifies the prim path to attach the XR anchor to for dynamic positioning. + + When set, the XR anchor will be attached to the specified prim (e.g., robot root prim), + allowing the XR camera to move with the prim. This is particularly useful for locomotion + robot teleoperation where the robot moves and the XR camera should follow it. + + If None, the anchor will use the static :attr:`anchor_pos` and :attr:`anchor_rot` values. + """ + + anchor_rotation_mode: XrAnchorRotationMode = XrAnchorRotationMode.FIXED + """Specifies how the XR anchor rotation should behave when attached to a prim. + + The available modes are: + - :attr:`XrAnchorRotationMode.FIXED`: Sets rotation once to anchor_rot value + - :attr:`XrAnchorRotationMode.FOLLOW_PRIM`: Rotation follows prim's rotation + - :attr:`XrAnchorRotationMode.FOLLOW_PRIM_SMOOTHED`: Rotation smoothly follows prim's rotation using slerp + - :attr:`XrAnchorRotationMode.CUSTOM`: user provided function to calculate the rotation + """ + + anchor_rotation_smoothing_time: float = 1.0 + """Wall-clock time constant (seconds) for rotation smoothing in FOLLOW_PRIM_SMOOTHED mode. + + This time constant is applied using wall-clock delta time between frames (not physics dt). + Smaller values (e.g., 0.1) result in faster/snappier response but less smoothing. + Larger values (e.g., 0.75–2.0) result in slower/smoother response but more lag. + Typical useful range: 0.3 – 1.5 seconds depending on runtime frame-rate and comfort. + """ + + anchor_rotation_custom_func: Callable[[np.ndarray, np.ndarray], np.ndarray] = lambda headpose, primpose: np.array( + [1, 0, 0, 0], dtype=np.float64 + ) + """Specifies the function to calculate the rotation of the XR anchor when anchor_rotation_mode is CUSTOM. + + Args: + headpose: Previous head pose as numpy array [x, y, z, w, x, y, z] (position + quaternion) + pose: Anchor prim pose as numpy array [x, y, z, w, x, y, z] (position + quaternion) + + Returns: + np.ndarray: Quaternion as numpy array [w, x, y, z] + """ + near_plane: float = 0.15 """Specifies the near plane distance for the XR device. This value determines the closest distance at which objects will be rendered in the XR device. """ + fixed_anchor_height: bool = True + """Specifies if the anchor height should be fixed. + + If True, the anchor height will be fixed to the initial height of the anchor prim. + """ + from typing import Any @@ -55,7 +124,10 @@ def remove_camera_configs(env_cfg: Any) -> Any: The modified environment configuration with cameras removed. """ - import omni.log + import logging + + # import logger + logger = logging.getLogger(__name__) from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import CameraCfg @@ -64,7 +136,7 @@ def remove_camera_configs(env_cfg: Any) -> Any: attr = getattr(env_cfg.scene, attr_name) if isinstance(attr, CameraCfg): delattr(env_cfg.scene, attr_name) - omni.log.info(f"Removed camera config: {attr_name}") + logger.info(f"Removed camera config: {attr_name}") # Remove any ObsTerms for the camera if hasattr(env_cfg.observations, "policy"): @@ -74,6 +146,6 @@ def remove_camera_configs(env_cfg: Any) -> Any: for param_value in obsterm.params.values(): if isinstance(param_value, SceneEntityCfg) and param_value.name == attr_name: delattr(env_cfg.observations.policy, attr_name) - omni.log.info(f"Removed camera observation term: {attr_name}") + logger.info(f"Removed camera observation term: {attr_name}") break return env_cfg diff --git a/source/isaaclab/isaaclab/devices/retargeter_base.py b/source/isaaclab/isaaclab/devices/retargeter_base.py index 6193966d7133..fcd443a155b2 100644 --- a/source/isaaclab/isaaclab/devices/retargeter_base.py +++ b/source/isaaclab/isaaclab/devices/retargeter_base.py @@ -1,10 +1,11 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from abc import ABC, abstractmethod from dataclasses import dataclass +from enum import Enum from typing import Any @@ -13,6 +14,8 @@ class RetargeterCfg: """Base configuration for hand tracking retargeters.""" sim_device: str = "cpu" + # Concrete retargeter class to construct for this config. Set by each retargeter module. + retargeter_type: type["RetargeterBase"] | None = None class RetargeterBase(ABC): @@ -34,6 +37,13 @@ def __init__(self, cfg: RetargeterCfg): """ self._sim_device = cfg.sim_device + class Requirement(Enum): + """Features a retargeter may require from a device's raw data feed.""" + + HAND_TRACKING = "hand_tracking" + HEAD_TRACKING = "head_tracking" + MOTION_CONTROLLER = "motion_controller" + @abstractmethod def retarget(self, data: Any) -> Any: """Retarget input data to desired output format. @@ -45,3 +55,15 @@ def retarget(self, data: Any) -> Any: Retargeted data in implementation-specific format """ pass + + def get_requirements(self) -> list["RetargeterBase.Requirement"]: + """Return the list of required data features for this retargeter. + + Defaults to requesting all available features for backward compatibility. + Implementations should override to narrow to only what they need. + """ + return [ + RetargeterBase.Requirement.HAND_TRACKING, + RetargeterBase.Requirement.HEAD_TRACKING, + RetargeterBase.Requirement.MOTION_CONTROLLER, + ] diff --git a/source/isaaclab/isaaclab/devices/spacemouse/__init__.py b/source/isaaclab/isaaclab/devices/spacemouse/__init__.py index 02fc965028b9..f3cc1c2fd9c4 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/__init__.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py b/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py index 190ddc19ebb5..b0d14b0469f3 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py @@ -1,34 +1,27 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Spacemouse controller for SE(2) control.""" -import hid -import numpy as np +from __future__ import annotations + import threading import time -import torch from collections.abc import Callable from dataclasses import dataclass +import hid +import numpy as np +import torch + from isaaclab.utils.array import convert_to_torch from ..device_base import DeviceBase, DeviceCfg from .utils import convert_buffer -@dataclass -class Se2SpaceMouseCfg(DeviceCfg): - """Configuration for SE2 space mouse devices.""" - - v_x_sensitivity: float = 0.8 - v_y_sensitivity: float = 0.4 - omega_z_sensitivity: float = 1.0 - sim_device: str = "cpu" - - class Se2SpaceMouse(DeviceBase): r"""A space-mouse controller for sending SE(2) commands as delta poses. @@ -168,3 +161,13 @@ def _run_device(self): # additional callbacks if "R" in self._additional_callbacks: self._additional_callbacks["R"] + + +@dataclass +class Se2SpaceMouseCfg(DeviceCfg): + """Configuration for SE2 space mouse devices.""" + + v_x_sensitivity: float = 0.8 + v_y_sensitivity: float = 0.4 + omega_z_sensitivity: float = 1.0 + class_type: type[DeviceBase] = Se2SpaceMouse diff --git a/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py b/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py index feb366ee4cdd..1bc7c00ae567 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py @@ -1,33 +1,26 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Spacemouse controller for SE(3) control.""" -import hid -import numpy as np +from __future__ import annotations + import threading import time -import torch from collections.abc import Callable from dataclasses import dataclass + +import hid +import numpy as np +import torch from scipy.spatial.transform import Rotation from ..device_base import DeviceBase, DeviceCfg from .utils import convert_buffer -@dataclass -class Se3SpaceMouseCfg(DeviceCfg): - """Configuration for SE3 space mouse devices.""" - - gripper_term: bool = True - pos_sensitivity: float = 0.4 - rot_sensitivity: float = 0.8 - retargeters: None = None - - class Se3SpaceMouse(DeviceBase): """A space-mouse controller for sending SE(3) commands as delta poses. @@ -210,3 +203,14 @@ def _run_device(self): self._additional_callbacks["R"]() if data[1] == 3: self._read_rotation = not self._read_rotation + + +@dataclass +class Se3SpaceMouseCfg(DeviceCfg): + """Configuration for SE3 space mouse devices.""" + + gripper_term: bool = True + pos_sensitivity: float = 0.4 + rot_sensitivity: float = 0.8 + retargeters: None = None + class_type: type[DeviceBase] = Se3SpaceMouse diff --git a/source/isaaclab/isaaclab/devices/spacemouse/utils.py b/source/isaaclab/isaaclab/devices/spacemouse/utils.py index be0d447f36b2..17510f70bce9 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/utils.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/devices/teleop_device_factory.py b/source/isaaclab/isaaclab/devices/teleop_device_factory.py index a02029645b6e..f7265c41c2c6 100644 --- a/source/isaaclab/isaaclab/devices/teleop_device_factory.py +++ b/source/isaaclab/isaaclab/devices/teleop_device_factory.py @@ -1,65 +1,20 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Factory to create teleoperation devices from configuration.""" -import contextlib import inspect +import logging from collections.abc import Callable - -import omni.log +from typing import cast from isaaclab.devices import DeviceBase, DeviceCfg -from isaaclab.devices.gamepad import Se2Gamepad, Se2GamepadCfg, Se3Gamepad, Se3GamepadCfg -from isaaclab.devices.keyboard import Se2Keyboard, Se2KeyboardCfg, Se3Keyboard, Se3KeyboardCfg -from isaaclab.devices.openxr.retargeters import ( - G1LowerBodyStandingRetargeter, - G1LowerBodyStandingRetargeterCfg, - G1TriHandUpperBodyRetargeter, - G1TriHandUpperBodyRetargeterCfg, - GR1T2Retargeter, - GR1T2RetargeterCfg, - GripperRetargeter, - GripperRetargeterCfg, - Se3AbsRetargeter, - Se3AbsRetargeterCfg, - Se3RelRetargeter, - Se3RelRetargeterCfg, - UnitreeG1Retargeter, - UnitreeG1RetargeterCfg, -) -from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg -from isaaclab.devices.spacemouse import Se2SpaceMouse, Se2SpaceMouseCfg, Se3SpaceMouse, Se3SpaceMouseCfg - -with contextlib.suppress(ModuleNotFoundError): - # May fail if xr is not in use - from isaaclab.devices.openxr import ManusVive, ManusViveCfg, OpenXRDevice, OpenXRDeviceCfg - -# Map device types to their constructor and expected config type -DEVICE_MAP: dict[type[DeviceCfg], type[DeviceBase]] = { - Se3KeyboardCfg: Se3Keyboard, - Se3SpaceMouseCfg: Se3SpaceMouse, - Se3GamepadCfg: Se3Gamepad, - Se2KeyboardCfg: Se2Keyboard, - Se2GamepadCfg: Se2Gamepad, - Se2SpaceMouseCfg: Se2SpaceMouse, - OpenXRDeviceCfg: OpenXRDevice, - ManusViveCfg: ManusVive, -} - - -# Map configuration types to their corresponding retargeter classes -RETARGETER_MAP: dict[type[RetargeterCfg], type[RetargeterBase]] = { - Se3AbsRetargeterCfg: Se3AbsRetargeter, - Se3RelRetargeterCfg: Se3RelRetargeter, - GripperRetargeterCfg: GripperRetargeter, - GR1T2RetargeterCfg: GR1T2Retargeter, - G1TriHandUpperBodyRetargeterCfg: G1TriHandUpperBodyRetargeter, - G1LowerBodyStandingRetargeterCfg: G1LowerBodyStandingRetargeter, - UnitreeG1RetargeterCfg: UnitreeG1Retargeter, -} +from isaaclab.devices.retargeter_base import RetargeterBase + +# import logger +logger = logging.getLogger(__name__) def create_teleop_device( @@ -86,39 +41,48 @@ def create_teleop_device( device_cfg = devices_cfg[device_name] callbacks = callbacks or {} - # Check if device config type is supported - cfg_type = type(device_cfg) - if cfg_type not in DEVICE_MAP: - raise ValueError(f"Unsupported device configuration type: {cfg_type.__name__}") - - # Get the constructor for this config type - constructor = DEVICE_MAP[cfg_type] + # Determine constructor from the configuration itself + device_constructor = getattr(device_cfg, "class_type", None) + if device_constructor is None: + raise ValueError( + f"Device configuration '{device_name}' does not declare class_type. " + "Set cfg.class_type to the concrete DeviceBase subclass." + ) + if not issubclass(device_constructor, DeviceBase): + raise TypeError(f"class_type for '{device_name}' must be a subclass of DeviceBase; got {device_constructor}") # Try to create retargeters if they are configured retargeters = [] if hasattr(device_cfg, "retargeters") and device_cfg.retargeters is not None: try: - # Create retargeters based on configuration + # Create retargeters based on configuration using per-config retargeter_type for retargeter_cfg in device_cfg.retargeters: - cfg_type = type(retargeter_cfg) - if cfg_type in RETARGETER_MAP: - retargeters.append(RETARGETER_MAP[cfg_type](retargeter_cfg)) - else: - raise ValueError(f"Unknown retargeter configuration type: {cfg_type.__name__}") + retargeter_constructor = getattr(retargeter_cfg, "retargeter_type", None) + if retargeter_constructor is None: + raise ValueError( + f"Retargeter configuration {type(retargeter_cfg).__name__} does not declare retargeter_type. " + "Set cfg.retargeter_type to the concrete RetargeterBase subclass." + ) + if not issubclass(retargeter_constructor, RetargeterBase): + raise TypeError( + f"retargeter_type for {type(retargeter_cfg).__name__} must be a subclass of RetargeterBase; got" + f" {retargeter_constructor}" + ) + retargeters.append(retargeter_constructor(retargeter_cfg)) except NameError as e: raise ValueError(f"Failed to create retargeters: {e}") - # Check if the constructor accepts retargeters parameter - constructor_params = inspect.signature(constructor).parameters - if "retargeters" in constructor_params and retargeters: - device = constructor(cfg=device_cfg, retargeters=retargeters) - else: - device = constructor(cfg=device_cfg) + # Build constructor kwargs based on signature + constructor_params = inspect.signature(device_constructor).parameters + params: dict = {"cfg": device_cfg} + if "retargeters" in constructor_params: + params["retargeters"] = retargeters + device = cast(DeviceBase, device_constructor(**params)) # Register callbacks for key, callback in callbacks.items(): device.add_callback(key, callback) - omni.log.info(f"Created teleoperation device: {device_name}") + logging.info(f"Created teleoperation device: {device_name}") return device diff --git a/source/isaaclab/isaaclab/envs/__init__.py b/source/isaaclab/isaaclab/envs/__init__.py index 2d274b8adad5..543ff2ad4bac 100644 --- a/source/isaaclab/isaaclab/envs/__init__.py +++ b/source/isaaclab/isaaclab/envs/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/common.py b/source/isaaclab/isaaclab/envs/common.py index 253b85b76a84..f913005d1dbb 100644 --- a/source/isaaclab/isaaclab/envs/common.py +++ b/source/isaaclab/isaaclab/envs/common.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from typing import Dict, Literal, TypeVar # noqa: UP035 + import gymnasium as gym import torch -from typing import Dict, Literal, TypeVar from isaaclab.utils import configclass @@ -44,7 +45,8 @@ class ViewerCfg: * ``"world"``: The origin of the world. * ``"env"``: The origin of the environment defined by :attr:`env_index`. * ``"asset_root"``: The center of the asset defined by :attr:`asset_name` in environment :attr:`env_index`. - * ``"asset_body"``: The center of the body defined by :attr:`body_name` in asset defined by :attr:`asset_name` in environment :attr:`env_index`. + * ``"asset_body"``: The center of the body defined by :attr:`body_name` in asset defined by + :attr:`asset_name` in environment :attr:`env_index`. """ env_index: int = 0 diff --git a/source/isaaclab/isaaclab/envs/direct_marl_env.py b/source/isaaclab/isaaclab/envs/direct_marl_env.py index 0e7429117fcd..206a4e7c01c3 100644 --- a/source/isaaclab/isaaclab/envs/direct_marl_env.py +++ b/source/isaaclab/isaaclab/envs/direct_marl_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,35 +6,39 @@ from __future__ import annotations import builtins -import gymnasium as gym import inspect +import logging import math -import numpy as np -import torch import weakref from abc import abstractmethod from collections.abc import Sequence from dataclasses import MISSING from typing import Any, ClassVar -import isaacsim.core.utils.torch as torch_utils +import gymnasium as gym +import numpy as np +import torch + import omni.kit.app -import omni.log import omni.physx -from isaacsim.core.version import get_version from isaaclab.managers import EventManager from isaaclab.scene import InteractiveScene from isaaclab.sim import SimulationContext -from isaaclab.sim.utils import attach_stage_to_usd_context, use_stage +from isaaclab.sim.utils.stage import attach_stage_to_usd_context, use_stage from isaaclab.utils.noise import NoiseModel +from isaaclab.utils.seed import configure_seed from isaaclab.utils.timer import Timer +from isaaclab.utils.version import get_isaac_sim_version from .common import ActionType, AgentID, EnvStepReturn, ObsType, StateType from .direct_marl_env_cfg import DirectMARLEnvCfg from .ui import ViewportCameraController from .utils.spaces import sample_space, spec_to_gym_space +# import logger +logger = logging.getLogger(__name__) + class DirectMARLEnv(gym.Env): """The superclass for the direct workflow to design multi-agent environments. @@ -60,7 +64,6 @@ class DirectMARLEnv(gym.Env): metadata: ClassVar[dict[str, Any]] = { "render_modes": [None, "human", "rgb_array"], - "isaac_sim_version": get_version(), } """Metadata for the environment.""" @@ -89,7 +92,7 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar if self.cfg.seed is not None: self.cfg.seed = self.seed(self.cfg.seed) else: - omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") + logger.warning("Seed not set for the environment. The environment creation may not be deterministic.") # create a simulation context to control the simulator if SimulationContext.instance() is None: @@ -115,7 +118,7 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar f"({self.cfg.decimation}). Multiple render calls will happen for each environment step." "If this is not intended, set the render interval to be equal to the decimation." ) - omni.log.warn(msg) + logger.warning(msg) # generate scene with Timer("[INFO]: Time taken for scene creation", "scene_creation"): @@ -157,7 +160,8 @@ def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwar self.sim.reset() # update scene to pre populate data buffers for assets and sensors. # this is needed for the observation manager to get valid tensors for initialization. - # this shouldn't cause an issue since later on, users do a reset over all the environments so the lazy buffers would be reset. + # this shouldn't cause an issue since later on, users do a reset over all the environments + # so the lazy buffers would be reset. self.scene.update(dt=self.physics_dt) # check if debug visualization is has been implemented by the environment @@ -356,7 +360,8 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn: Shape of individual tensors is (num_envs, action_dim). Returns: - A tuple containing the observations, rewards, resets (terminated and truncated) and extras (keyed by the agent ID). + A tuple containing the observations, rewards, resets (terminated and truncated) and + extras (keyed by the agent ID). Shape of individual tensors is (num_envs, ...). """ actions = {agent: action.to(self.device) for agent, action in actions.items()} @@ -462,7 +467,7 @@ def seed(seed: int = -1) -> int: except ModuleNotFoundError: pass # set seed for torch and other libraries - return torch_utils.set_seed(seed) + return configure_seed(seed) def render(self, recompute: bool = False) -> np.ndarray | None: """Run rendering without stepping through the physics. @@ -540,7 +545,7 @@ def close(self): del self.viewport_camera_controller # clear callbacks and instance - if float(".".join(get_version()[2])) >= 5: + if get_isaac_sim_version().major >= 5: if self.cfg.sim.create_stage_in_memory: # detach physx stage omni.physx.get_physx_simulation_interface().detach_stage() @@ -602,17 +607,17 @@ def _configure_env_spaces(self): # show deprecation message and overwrite configuration if self.cfg.num_actions is not None: - omni.log.warn("DirectMARLEnvCfg.num_actions is deprecated. Use DirectMARLEnvCfg.action_spaces instead.") + logger.warning("DirectMARLEnvCfg.num_actions is deprecated. Use DirectMARLEnvCfg.action_spaces instead.") if isinstance(self.cfg.action_spaces, type(MISSING)): self.cfg.action_spaces = self.cfg.num_actions if self.cfg.num_observations is not None: - omni.log.warn( + logger.warning( "DirectMARLEnvCfg.num_observations is deprecated. Use DirectMARLEnvCfg.observation_spaces instead." ) if isinstance(self.cfg.observation_spaces, type(MISSING)): self.cfg.observation_spaces = self.cfg.num_observations if self.cfg.num_states is not None: - omni.log.warn("DirectMARLEnvCfg.num_states is deprecated. Use DirectMARLEnvCfg.state_space instead.") + logger.warning("DirectMARLEnvCfg.num_states is deprecated. Use DirectMARLEnvCfg.state_space instead.") if isinstance(self.cfg.state_space, type(MISSING)): self.cfg.state_space = self.cfg.num_states diff --git a/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py b/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py index 15f57cb4c033..66b2bcf998d6 100644 --- a/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -175,7 +175,8 @@ class DirectMARLEnvCfg: """ observation_noise_model: dict[AgentID, NoiseModelCfg | None] | None = None - """The noise model to apply to the computed observations from the environment. Default is None, which means no noise is added. + """The noise model to apply to the computed observations from the environment. Default is None, + which means no noise is added. Please refer to the :class:`isaaclab.utils.noise.NoiseModel` class for more details. """ @@ -212,7 +213,8 @@ class DirectMARLEnvCfg: """ action_noise_model: dict[AgentID, NoiseModelCfg | None] | None = None - """The noise model applied to the actions provided to the environment. Default is None, which means no noise is added. + """The noise model applied to the actions provided to the environment. Default is None, + which means no noise is added. Please refer to the :class:`isaaclab.utils.noise.NoiseModel` class for more details. """ diff --git a/source/isaaclab/isaaclab/envs/direct_rl_env.py b/source/isaaclab/isaaclab/envs/direct_rl_env.py index e43c4db7a286..69be0edb78dd 100644 --- a/source/isaaclab/isaaclab/envs/direct_rl_env.py +++ b/source/isaaclab/isaaclab/envs/direct_rl_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,36 +6,41 @@ from __future__ import annotations import builtins -import gymnasium as gym import inspect +import logging import math -import numpy as np -import torch +import warnings import weakref from abc import abstractmethod from collections.abc import Sequence from dataclasses import MISSING from typing import Any, ClassVar -import isaacsim.core.utils.torch as torch_utils +import gymnasium as gym +import numpy as np +import torch + import omni.kit.app -import omni.log import omni.physx from isaacsim.core.simulation_manager import SimulationManager -from isaacsim.core.version import get_version from isaaclab.managers import EventManager from isaaclab.scene import InteractiveScene from isaaclab.sim import SimulationContext -from isaaclab.sim.utils import attach_stage_to_usd_context, use_stage +from isaaclab.sim.utils.stage import attach_stage_to_usd_context, use_stage from isaaclab.utils.noise import NoiseModel +from isaaclab.utils.seed import configure_seed from isaaclab.utils.timer import Timer +from isaaclab.utils.version import get_isaac_sim_version from .common import VecEnvObs, VecEnvStepReturn from .direct_rl_env_cfg import DirectRLEnvCfg from .ui import ViewportCameraController from .utils.spaces import sample_space, spec_to_gym_space +# import logger +logger = logging.getLogger(__name__) + class DirectRLEnv(gym.Env): """The superclass for the direct workflow to design environments. @@ -66,7 +71,6 @@ class DirectRLEnv(gym.Env): """Whether the environment is a vectorized environment.""" metadata: ClassVar[dict[str, Any]] = { "render_modes": [None, "human", "rgb_array"], - "isaac_sim_version": get_version(), } """Metadata for the environment.""" @@ -95,7 +99,7 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs if self.cfg.seed is not None: self.cfg.seed = self.seed(self.cfg.seed) else: - omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") + logger.warning("Seed not set for the environment. The environment creation may not be deterministic.") # create a simulation context to control the simulator if SimulationContext.instance() is None: @@ -121,7 +125,7 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs f"({self.cfg.decimation}). Multiple render calls will happen for each environment step." "If this is not intended, set the render interval to be equal to the decimation." ) - omni.log.warn(msg) + logger.warning(msg) # generate scene with Timer("[INFO]: Time taken for scene creation", "scene_creation"): @@ -163,7 +167,8 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs self.sim.reset() # update scene to pre populate data buffers for assets and sensors. # this is needed for the observation manager to get valid tensors for initialization. - # this shouldn't cause an issue since later on, users do a reset over all the environments so the lazy buffers would be reset. + # this shouldn't cause an issue since later on, users do a reset over all the environments + # so the lazy buffers would be reset. self.scene.update(dt=self.physics_dt) # check if debug visualization is has been implemented by the environment @@ -219,6 +224,20 @@ def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs # video matches the simulation self.metadata["render_fps"] = 1 / self.step_dt + # show deprecation message for rerender_on_reset + if self.cfg.rerender_on_reset: + msg = ( + "\033[93m\033[1m[DEPRECATION WARNING] DirectRLEnvCfg.rerender_on_reset is deprecated. Use" + " DirectRLEnvCfg.num_rerenders_on_reset instead.\033[0m" + ) + warnings.warn( + msg, + FutureWarning, + stacklevel=2, + ) + if self.cfg.num_rerenders_on_reset == 0: + self.cfg.num_rerenders_on_reset = 1 + # print the environment information print("[INFO]: Completed setting up the environment...") @@ -300,8 +319,9 @@ def reset(self, seed: int | None = None, options: dict[str, Any] | None = None) self.sim.forward() # if sensors are added to the scene, make sure we render to reflect changes in reset - if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: - self.sim.render() + if self.sim.has_rtx_sensors() and self.cfg.num_rerenders_on_reset > 0: + for _ in range(self.cfg.num_rerenders_on_reset): + self.sim.render() if self.cfg.wait_for_textures and self.sim.has_rtx_sensors(): while SimulationManager.assets_loading(): @@ -377,8 +397,9 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: if len(reset_env_ids) > 0: self._reset_idx(reset_env_ids) # if sensors are added to the scene, make sure we render to reflect changes in reset - if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: - self.sim.render() + if self.sim.has_rtx_sensors() and self.cfg.num_rerenders_on_reset > 0: + for _ in range(self.cfg.num_rerenders_on_reset): + self.sim.render() # post-step: step interval event if self.cfg.events: @@ -414,7 +435,7 @@ def seed(seed: int = -1) -> int: except ModuleNotFoundError: pass # set seed for torch and other libraries - return torch_utils.set_seed(seed) + return configure_seed(seed) def render(self, recompute: bool = False) -> np.ndarray | None: """Run rendering without stepping through the physics. @@ -492,7 +513,7 @@ def close(self): del self.viewport_camera_controller # clear callbacks and instance - if float(".".join(get_version()[2])) >= 5: + if get_isaac_sim_version().major >= 5: if self.cfg.sim.create_stage_in_memory: # detach physx stage omni.physx.get_physx_simulation_interface().detach_stage() @@ -551,17 +572,17 @@ def _configure_gym_env_spaces(self): """Configure the action and observation spaces for the Gym environment.""" # show deprecation message and overwrite configuration if self.cfg.num_actions is not None: - omni.log.warn("DirectRLEnvCfg.num_actions is deprecated. Use DirectRLEnvCfg.action_space instead.") + logger.warning("DirectRLEnvCfg.num_actions is deprecated. Use DirectRLEnvCfg.action_space instead.") if isinstance(self.cfg.action_space, type(MISSING)): self.cfg.action_space = self.cfg.num_actions if self.cfg.num_observations is not None: - omni.log.warn( + logger.warning( "DirectRLEnvCfg.num_observations is deprecated. Use DirectRLEnvCfg.observation_space instead." ) if isinstance(self.cfg.observation_space, type(MISSING)): self.cfg.observation_space = self.cfg.num_observations if self.cfg.num_states is not None: - omni.log.warn("DirectRLEnvCfg.num_states is deprecated. Use DirectRLEnvCfg.state_space instead.") + logger.warning("DirectRLEnvCfg.num_states is deprecated. Use DirectRLEnvCfg.state_space instead.") if isinstance(self.cfg.state_space, type(MISSING)): self.cfg.state_space = self.cfg.num_states diff --git a/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py b/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py index 33297a228afd..a1ebb883c65b 100644 --- a/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -170,7 +170,8 @@ class DirectRLEnvCfg: """ observation_noise_model: NoiseModelCfg | None = None - """The noise model to apply to the computed observations from the environment. Default is None, which means no noise is added. + """The noise model to apply to the computed observations from the environment. Default is None, + which means no noise is added. Please refer to the :class:`isaaclab.utils.noise.NoiseModel` class for more details. """ @@ -207,7 +208,8 @@ class DirectRLEnvCfg: """ action_noise_model: NoiseModelCfg | None = None - """The noise model applied to the actions provided to the environment. Default is None, which means no noise is added. + """The noise model applied to the actions provided to the environment. Default is None, + which means no noise is added. Please refer to the :class:`isaaclab.utils.noise.NoiseModel` class for more details. """ @@ -222,6 +224,23 @@ class DirectRLEnvCfg: to reflect the latest states from the reset. This comes at a cost of performance as an additional render step will be performed after each time an environment is reset. + .. deprecated:: 2.3.1 + This attribute is deprecated and will be removed in the future. Please use + :attr:`num_rerenders_on_reset` instead. + + To get the same behaviour as setting this parameter to ``True`` or ``False``, set + :attr:`num_rerenders_on_reset` to 1 or 0, respectively. + """ + + num_rerenders_on_reset: int = 0 + """Number of render steps to perform after reset. Defaults to 0, which means no render step will be performed + after reset. + + * When this is 0, no render step will be performed after reset. Data collected from sensors after performing + reset will be stale and will not reflect the latest states in simulation caused by the reset. + * When this is greater than 0, the specified number of extra render steps will be performed to update the + sensor data to reflect the latest states from the reset. This comes at a cost of performance as additional + render steps will be performed after each time an environment is reset. """ wait_for_textures: bool = True diff --git a/source/isaaclab/isaaclab/envs/manager_based_env.py b/source/isaaclab/isaaclab/envs/manager_based_env.py index 9ddc538aa418..3ff6d291c0a7 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_env.py @@ -1,34 +1,40 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 builtins -import torch +import logging +import warnings from collections.abc import Sequence from typing import Any -import isaacsim.core.utils.torch as torch_utils -import omni.log +import torch + import omni.physx from isaacsim.core.simulation_manager import SimulationManager -from isaacsim.core.version import get_version from isaaclab.managers import ActionManager, EventManager, ObservationManager, RecorderManager from isaaclab.scene import InteractiveScene from isaaclab.sim import SimulationContext -from isaaclab.sim.utils import attach_stage_to_usd_context, use_stage +from isaaclab.sim.utils.stage import attach_stage_to_usd_context, use_stage from isaaclab.ui.widgets import ManagerLiveVisualizer +from isaaclab.utils.seed import configure_seed from isaaclab.utils.timer import Timer +from isaaclab.utils.version import get_isaac_sim_version from .common import VecEnvObs from .manager_based_env_cfg import ManagerBasedEnvCfg from .ui import ViewportCameraController from .utils.io_descriptors import export_articulations_data, export_scene_data +# import logger +logger = logging.getLogger(__name__) + class ManagerBasedEnv: - """The base environment encapsulates the simulation scene and the environment managers for the manager-based workflow. + """The base environment encapsulates the simulation scene and the environment managers for + the manager-based workflow. While a simulation scene or world comprises of different components such as the robots, objects, and sensors (cameras, lidars, etc.), the environment is a higher level abstraction @@ -89,7 +95,7 @@ def __init__(self, cfg: ManagerBasedEnvCfg): if self.cfg.seed is not None: self.cfg.seed = self.seed(self.cfg.seed) else: - omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") + logger.warning("Seed not set for the environment. The environment creation may not be deterministic.") # create a simulation context to control the simulator if SimulationContext.instance() is None: @@ -121,7 +127,7 @@ def __init__(self, cfg: ManagerBasedEnvCfg): f"({self.cfg.decimation}). Multiple render calls will happen for each environment step. " "If this is not intended, set the render interval to be equal to the decimation." ) - omni.log.warn(msg) + logger.warning(msg) # counter for simulation steps self._sim_step_counter = 0 @@ -167,7 +173,8 @@ def __init__(self, cfg: ManagerBasedEnvCfg): self.sim.reset() # update scene to pre populate data buffers for assets and sensors. # this is needed for the observation manager to get valid tensors for initialization. - # this shouldn't cause an issue since later on, users do a reset over all the environments so the lazy buffers would be reset. + # this shouldn't cause an issue since later on, users do a reset over all the environments + # so the lazy buffers would be reset. self.scene.update(dt=self.physics_dt) # add timeline event to load managers self.load_managers() @@ -190,6 +197,20 @@ def __init__(self, cfg: ManagerBasedEnvCfg): if self.cfg.export_io_descriptors: self.export_IO_descriptors() + # show deprecation message for rerender_on_reset + if self.cfg.rerender_on_reset: + msg = ( + "\033[93m\033[1m[DEPRECATION WARNING] ManagerBasedEnvCfg.rerender_on_reset is deprecated. Use" + " ManagerBasedEnvCfg.num_rerenders_on_reset instead.\033[0m" + ) + warnings.warn( + msg, + FutureWarning, + stacklevel=2, + ) + if self.cfg.num_rerenders_on_reset == 0: + self.cfg.num_rerenders_on_reset = 1 + def __del__(self): """Cleanup for the environment.""" self.close() @@ -245,6 +266,7 @@ def export_IO_descriptors(self, output_dir: str | None = None): output_dir: The directory to export the IO descriptors to. """ import os + import yaml IO_descriptors = self.get_IO_descriptors @@ -353,8 +375,9 @@ def reset( self.scene.write_data_to_sim() self.sim.forward() # if sensors are added to the scene, make sure we render to reflect changes in reset - if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: - self.sim.render() + if self.sim.has_rtx_sensors() and self.cfg.num_rerenders_on_reset > 0: + for _ in range(self.cfg.num_rerenders_on_reset): + self.sim.render() # trigger recorder terms for post-reset calls self.recorder_manager.record_post_reset(env_ids) @@ -413,8 +436,9 @@ def reset_to( self.sim.forward() # if sensors are added to the scene, make sure we render to reflect changes in reset - if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: - self.sim.render() + if self.sim.has_rtx_sensors() and self.cfg.num_rerenders_on_reset > 0: + for _ in range(self.cfg.num_rerenders_on_reset): + self.sim.render() # trigger recorder terms for post-reset calls self.recorder_manager.record_post_reset(env_ids) @@ -495,7 +519,7 @@ def seed(seed: int = -1) -> int: except ModuleNotFoundError: pass # set seed for torch and other libraries - return torch_utils.set_seed(seed) + return configure_seed(seed) def close(self): """Cleanup for the environment.""" @@ -509,7 +533,7 @@ def close(self): del self.scene # clear callbacks and instance - if float(".".join(get_version()[2])) >= 5: + if get_isaac_sim_version().major >= 5: if self.cfg.sim.create_stage_in_memory: # detach physx stage omni.physx.get_physx_simulation_interface().detach_stage() diff --git a/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py b/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py index a7200a3d1d2d..e8f583ddfb31 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -115,6 +115,23 @@ class ManagerBasedEnvCfg: to reflect the latest states from the reset. This comes at a cost of performance as an additional render step will be performed after each time an environment is reset. + .. deprecated:: 2.3.1 + This attribute is deprecated and will be removed in the future. Please use + :attr:`num_rerenders_on_reset` instead. + + To get the same behaviour as setting this parameter to ``True`` or ``False``, set + :attr:`num_rerenders_on_reset` to 1 or 0, respectively. + """ + + num_rerenders_on_reset: int = 0 + """Number of render steps to perform after reset. Defaults to 0, which means no render step will be + performed after reset. + + * When this is 0, no render step will be performed after reset. Data collected from sensors after performing + reset will be stale and will not reflect the latest states in simulation caused by the reset. + * When this is greater than 0, the specified number of extra render steps will be performed to update the + sensor data to reflect the latest states from the reset. This comes at a cost of performance as additional + render steps will be performed after each time an environment is reset. """ wait_for_textures: bool = True diff --git a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py index 634bec4cae95..ab8c04155d2d 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,14 +6,13 @@ # needed to import for allowing type-hinting: np.ndarray | None from __future__ import annotations -import gymnasium as gym import math -import numpy as np -import torch from collections.abc import Sequence from typing import Any, ClassVar -from isaacsim.core.version import get_version +import gymnasium as gym +import numpy as np +import torch from isaaclab.managers import CommandManager, CurriculumManager, RewardManager, TerminationManager from isaaclab.ui.widgets import ManagerLiveVisualizer @@ -57,7 +56,6 @@ class ManagerBasedRLEnv(ManagerBasedEnv, gym.Env): """Whether the environment is a vectorized environment.""" metadata: ClassVar[dict[str, Any]] = { "render_modes": [None, "human", "rgb_array"], - "isaac_sim_version": get_version(), } """Metadata for the environment.""" @@ -84,7 +82,8 @@ def __init__(self, cfg: ManagerBasedRLEnvCfg, render_mode: str | None = None, ** self.render_mode = render_mode # initialize data and constants - # -- set the framerate of the gym video recorder wrapper so that the playback speed of the produced video matches the simulation + # -- set the framerate of the gym video recorder wrapper so that the playback speed of the + # produced video matches the simulation self.metadata["render_fps"] = 1 / self.step_dt print("[INFO]: Completed setting up the environment...") @@ -222,8 +221,9 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: self._reset_idx(reset_env_ids) # if sensors are added to the scene, make sure we render to reflect changes in reset - if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: - self.sim.render() + if self.sim.has_rtx_sensors() and self.cfg.num_rerenders_on_reset > 0: + for _ in range(self.cfg.num_rerenders_on_reset): + self.sim.render() # trigger recorder terms for post-reset calls self.recorder_manager.record_post_reset(reset_env_ids) diff --git a/source/isaaclab/isaaclab/envs/manager_based_rl_env_cfg.py b/source/isaaclab/isaaclab/envs/manager_based_rl_env_cfg.py index cabc6e58245b..eac633e8b99c 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_rl_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/manager_based_rl_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py b/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py index 781f89ccbeb0..8518d716332e 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py @@ -1,16 +1,23 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Sequence +import torch + import isaaclab.utils.math as PoseUtils from isaaclab.envs import ManagerBasedRLEnv +def optional_method(func): + """Decorator to mark a method as optional.""" + func.__is_optional__ = True + return func + + class ManagerBasedRLMimicEnv(ManagerBasedRLEnv): """The superclass for the Isaac Lab Mimic environments. @@ -156,3 +163,21 @@ def serialize(self): and used in utils/env_utils.py. """ return dict(env_name=self.spec.id, type=2, env_kwargs=dict()) + + @optional_method + def get_navigation_state(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """ + Optional method. Only required when using navigation controller locomanipulation data generation. + + Gets the navigation state of the robot. Required when use of the navigation controller is + enabled. The navigation state includes a boolean flag "is_navigating" to indicate when the + robot is under control by the navigation controller, and a boolean flag "navigation_goal_reached" + to indicate when the navigation goal has been reached. + + Args: + env_ids: The environment index to get the navigation state for. If None, all envs are considered. + + Returns: + A dictionary that of navigation state flags (False or True). + """ + raise NotImplementedError diff --git a/source/isaaclab/isaaclab/envs/mdp/__init__.py b/source/isaaclab/isaaclab/envs/mdp/__init__.py index 73be1dbe4628..bdb507f3eb0a 100644 --- a/source/isaaclab/isaaclab/envs/mdp/__init__.py +++ b/source/isaaclab/isaaclab/envs/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/actions/__init__.py b/source/isaaclab/isaaclab/envs/mdp/actions/__init__.py index ad1178be44d0..56b3ae25ff4e 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/__init__.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/actions/actions_cfg.py b/source/isaaclab/isaaclab/envs/mdp/actions/actions_cfg.py index 19a84846a521..bf8748bdf3ad 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/actions_cfg.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/actions_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -131,6 +131,9 @@ class JointPositionToLimitsActionCfg(ActionTermCfg): This operation is performed after applying the scale factor. """ + preserve_order: bool = False + """Whether to preserve the order of the joint names in the action output. Defaults to False.""" + @configclass class EMAJointPositionToLimitsActionCfg(JointPositionToLimitsActionCfg): diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py index 501c221dec6a..289045bd37ba 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch import isaaclab.utils.string as string_utils from isaaclab.assets.articulation import Articulation @@ -21,6 +21,9 @@ from . import actions_cfg +# import logger +logger = logging.getLogger(__name__) + class BinaryJointAction(ActionTerm): """Base class for binary joint actions. @@ -54,7 +57,7 @@ def __init__(self, cfg: actions_cfg.BinaryJointActionCfg, env: ManagerBasedEnv) self._joint_ids, self._joint_names = self._asset.find_joints(self.cfg.joint_names) self._num_joints = len(self._joint_ids) # log the resolved joint names for debugging - omni.log.info( + logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py index 8d5e7ebd4b3c..7ca7fe66c4b4 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch import isaaclab.utils.string as string_utils from isaaclab.assets.articulation import Articulation @@ -21,6 +21,9 @@ from . import actions_cfg +# import logger +logger = logging.getLogger(__name__) + class JointAction(ActionTerm): r"""Base class for joint actions. @@ -64,7 +67,7 @@ def __init__(self, cfg: actions_cfg.JointActionCfg, env: ManagerBasedEnv) -> Non ) self._num_joints = len(self._joint_ids) # log the resolved joint names for debugging - omni.log.info( + logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py index 2667f7e86a64..3fc50ef2d712 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils @@ -22,6 +22,9 @@ from . import actions_cfg +# import logger +logger = logging.getLogger(__name__) + class JointPositionToLimitsAction(ActionTerm): """Joint position action term that scales the input actions to the joint limits and applies them to the @@ -53,10 +56,12 @@ def __init__(self, cfg: actions_cfg.JointPositionToLimitsActionCfg, env: Manager super().__init__(cfg, env) # resolve the joints over which the action term is applied - self._joint_ids, self._joint_names = self._asset.find_joints(self.cfg.joint_names) + self._joint_ids, self._joint_names = self._asset.find_joints( + self.cfg.joint_names, preserve_order=cfg.preserve_order + ) self._num_joints = len(self._joint_ids) # log the resolved joint names for debugging - omni.log.info( + logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) @@ -75,17 +80,22 @@ def __init__(self, cfg: actions_cfg.JointPositionToLimitsActionCfg, env: Manager elif isinstance(cfg.scale, dict): self._scale = torch.ones(self.num_envs, self.action_dim, device=self.device) # resolve the dictionary config - index_list, _, value_list = string_utils.resolve_matching_names_values(self.cfg.scale, self._joint_names) + index_list, _, value_list = string_utils.resolve_matching_names_values( + self.cfg.scale, self._joint_names, preserve_order=cfg.preserve_order + ) self._scale[:, index_list] = torch.tensor(value_list, device=self.device) else: raise ValueError(f"Unsupported scale type: {type(cfg.scale)}. Supported types are float and dict.") + # parse clip if self.cfg.clip is not None: if isinstance(cfg.clip, dict): self._clip = torch.tensor([[-float("inf"), float("inf")]], device=self.device).repeat( self.num_envs, self.action_dim, 1 ) - index_list, _, value_list = string_utils.resolve_matching_names_values(self.cfg.clip, self._joint_names) + index_list, _, value_list = string_utils.resolve_matching_names_values( + self.cfg.clip, self._joint_names, preserve_order=cfg.preserve_order + ) self._clip[:, index_list] = torch.tensor(value_list, device=self.device) else: raise ValueError(f"Unsupported clip type: {type(cfg.clip)}. Supported types are dict.") @@ -181,7 +191,9 @@ class EMAJointPositionToLimitsAction(JointPositionToLimitsAction): .. math:: - \text{applied action} = \alpha \times \text{processed actions} + (1 - \alpha) \times \text{previous applied action} + \text{applied action} = + \alpha \times \text{processed actions} + + (1 - \alpha) \times \text{previous applied action} where :math:`\alpha` is the weight for the moving average, :math:`\text{processed actions}` are the processed actions, and :math:`\text{previous action}` is the previous action that was applied to the articulation's @@ -257,12 +269,12 @@ def IO_descriptor(self) -> GenericActionIODescriptor: def reset(self, env_ids: Sequence[int] | None = None) -> None: # check if specific environment ids are provided if env_ids is None: - env_ids = slice(None) + super().reset(slice(None)) + self._prev_applied_actions[:] = self._asset.data.joint_pos[:, self._joint_ids] else: - env_ids = env_ids[:, None] - super().reset(env_ids) - # reset history to current joint positions - self._prev_applied_actions[env_ids, :] = self._asset.data.joint_pos[env_ids, self._joint_ids] + super().reset(env_ids) + curr_applied_actions = self._asset.data.joint_pos[env_ids[:, None], self._joint_ids].view(len(env_ids), -1) + self._prev_applied_actions[env_ids, :] = curr_applied_actions def process_actions(self, actions: torch.Tensor): # apply affine transformations diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py index 1fb50f1fea8f..0a6c65f91022 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch import isaaclab.utils.string as string_utils from isaaclab.assets.articulation import Articulation @@ -22,6 +22,9 @@ from . import actions_cfg +# import logger +logger = logging.getLogger(__name__) + class NonHolonomicAction(ActionTerm): r"""Non-holonomic action that maps a two dimensional action to the velocity of the robot in @@ -92,11 +95,11 @@ def __init__(self, cfg: actions_cfg.NonHolonomicActionCfg, env: ManagerBasedEnv) self._joint_ids = [x_joint_id[0], y_joint_id[0], yaw_joint_id[0]] self._joint_names = [x_joint_name[0], y_joint_name[0], yaw_joint_name[0]] # log info for debugging - omni.log.info( + logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) - omni.log.info( + logger.info( f"Resolved body name for the action term {self.__class__.__name__}: {self._body_name} [{self._body_idx}]" ) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/pink_actions_cfg.py b/source/isaaclab/isaaclab/envs/mdp/actions/pink_actions_cfg.py index db478e7186e0..a82433be84cc 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/pink_actions_cfg.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/pink_actions_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/actions/pink_task_space_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/pink_task_space_actions.py index 79490c07e426..8ae90af3a4a2 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/pink_task_space_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/pink_task_space_actions.py @@ -1,14 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch from pink.tasks import FrameTask import isaaclab.utils.math as math_utils diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_actions_cfg.py b/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_actions_cfg.py index 00bcc453d9cf..dfd7a72dcb73 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_actions_cfg.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_actions_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py index b270a7b92c1c..83e7bd3e3654 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils @@ -22,8 +22,12 @@ from . import rmpflow_actions_cfg +# import logger +logger = logging.getLogger(__name__) + class RMPFlowAction(ActionTerm): + """RMPFlow task space action term.""" cfg: rmpflow_actions_cfg.RMPFlowActionCfg """The configuration of the action term.""" @@ -62,11 +66,11 @@ def __init__(self, cfg: rmpflow_actions_cfg.RMPFlowActionCfg, env: ManagerBasedE self._jacobi_joint_ids = [i + 6 for i in self._joint_ids] # log info for debugging - omni.log.info( + logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) - omni.log.info( + logger.info( f"Resolved body name for the action term {self.__class__.__name__}: {self._body_name} [{self._body_idx}]" ) # Avoid indexing across all joints for efficiency diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py index 4313fbeacd2a..f16d1403853b 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch from isaaclab.assets.surface_gripper import SurfaceGripper from isaaclab.managers.action_manager import ActionTerm @@ -19,6 +19,9 @@ from . import actions_cfg +# import logger +logger = logging.getLogger(__name__) + class SurfaceGripperBinaryAction(ActionTerm): """Surface gripper binary action. @@ -48,7 +51,7 @@ def __init__(self, cfg: actions_cfg.SurfaceGripperBinaryActionCfg, env: ManagerB super().__init__(cfg, env) # log the resolved asset name for debugging - omni.log.info( + logger.info( f"Resolved surface gripper asset for the action term {self.__class__.__name__}: {self.cfg.asset_name}" ) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py index a764c1f0affc..47f9cec23490 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py @@ -1,15 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch + from pxr import UsdPhysics import isaaclab.utils.math as math_utils @@ -27,6 +28,9 @@ from . import actions_cfg +# import logger +logger = logging.getLogger(__name__) + class DifferentialInverseKinematicsAction(ActionTerm): r"""Inverse Kinematics action term. @@ -78,11 +82,11 @@ def __init__(self, cfg: actions_cfg.DifferentialInverseKinematicsActionCfg, env: self._jacobi_joint_ids = [i + 6 for i in self._joint_ids] # log info for debugging - omni.log.info( + logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) - omni.log.info( + logger.info( f"Resolved body name for the action term {self.__class__.__name__}: {self._body_name} [{self._body_idx}]" ) # Avoid indexing across all joints for efficiency @@ -306,11 +310,11 @@ def __init__(self, cfg: actions_cfg.OperationalSpaceControllerActionCfg, env: Ma self._jacobi_joint_idx = [i + 6 for i in self._joint_ids] # log info for debugging - omni.log.info( + logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) - omni.log.info( + logger.info( f"Resolved ee body name for the action term {self.__class__.__name__}:" f" {self._ee_body_name} [{self._ee_body_idx}]" ) @@ -662,10 +666,14 @@ def _compute_ee_jacobian(self): # v_link = v_ee + w_ee x r_link_ee = v_J_ee * q + w_J_ee * q x r_link_ee # = (v_J_ee + w_J_ee x r_link_ee ) * q # = (v_J_ee - r_link_ee_[x] @ w_J_ee) * q - self._jacobian_b[:, 0:3, :] += torch.bmm(-math_utils.skew_symmetric_matrix(self._offset_pos), self._jacobian_b[:, 3:, :]) # type: ignore + self._jacobian_b[:, 0:3, :] += torch.bmm( + -math_utils.skew_symmetric_matrix(self._offset_pos), self._jacobian_b[:, 3:, :] + ) # type: ignore # -- rotational part # w_link = R_link_ee @ w_ee - self._jacobian_b[:, 3:, :] = torch.bmm(math_utils.matrix_from_quat(self._offset_rot), self._jacobian_b[:, 3:, :]) # type: ignore + self._jacobian_b[:, 3:, :] = torch.bmm( + math_utils.matrix_from_quat(self._offset_rot), self._jacobian_b[:, 3:, :] + ) # type: ignore def _compute_ee_pose(self): """Computes the pose of the ee frame in root frame.""" @@ -763,9 +771,9 @@ def _preprocess_actions(self, actions: torch.Tensor): max=self.cfg.controller_cfg.motion_stiffness_limits_task[1], ) if self._damping_ratio_idx is not None: - self._processed_actions[ - :, self._damping_ratio_idx : self._damping_ratio_idx + 6 - ] *= self._damping_ratio_scale + self._processed_actions[:, self._damping_ratio_idx : self._damping_ratio_idx + 6] *= ( + self._damping_ratio_scale + ) self._processed_actions[:, self._damping_ratio_idx : self._damping_ratio_idx + 6] = torch.clamp( self._processed_actions[:, self._damping_ratio_idx : self._damping_ratio_idx + 6], min=self.cfg.controller_cfg.motion_damping_ratio_limits_task[0], diff --git a/source/isaaclab/isaaclab/envs/mdp/commands/__init__.py b/source/isaaclab/isaaclab/envs/mdp/commands/__init__.py index c2e1a592dbcd..bdfce40473b2 100644 --- a/source/isaaclab/isaaclab/envs/mdp/commands/__init__.py +++ b/source/isaaclab/isaaclab/envs/mdp/commands/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/commands/commands_cfg.py b/source/isaaclab/isaaclab/envs/mdp/commands/commands_cfg.py index 2f558183dd81..390980f3454d 100644 --- a/source/isaaclab/isaaclab/envs/mdp/commands/commands_cfg.py +++ b/source/isaaclab/isaaclab/envs/mdp/commands/commands_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/commands/null_command.py b/source/isaaclab/isaaclab/envs/mdp/commands/null_command.py index 93df78155988..0fc2a378810f 100644 --- a/source/isaaclab/isaaclab/envs/mdp/commands/null_command.py +++ b/source/isaaclab/isaaclab/envs/mdp/commands/null_command.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/commands/pose_2d_command.py b/source/isaaclab/isaaclab/envs/mdp/commands/pose_2d_command.py index 76a078102ffc..a10ee0473e4a 100644 --- a/source/isaaclab/isaaclab/envs/mdp/commands/pose_2d_command.py +++ b/source/isaaclab/isaaclab/envs/mdp/commands/pose_2d_command.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +7,11 @@ from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation from isaaclab.managers import CommandTerm from isaaclab.markers import VisualizationMarkers diff --git a/source/isaaclab/isaaclab/envs/mdp/commands/pose_command.py b/source/isaaclab/isaaclab/envs/mdp/commands/pose_command.py index af831d6f92de..2c62c4baf4b8 100644 --- a/source/isaaclab/isaaclab/envs/mdp/commands/pose_command.py +++ b/source/isaaclab/isaaclab/envs/mdp/commands/pose_command.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +7,11 @@ from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation from isaaclab.managers import CommandTerm from isaaclab.markers import VisualizationMarkers diff --git a/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py b/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py index 64d22b4003fa..38bc076a9591 100644 --- a/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py +++ b/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,11 @@ from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation @@ -23,6 +23,9 @@ from .commands_cfg import NormalVelocityCommandCfg, UniformVelocityCommandCfg +# import logger +logger = logging.getLogger(__name__) + class UniformVelocityCommand(CommandTerm): r"""Command generator that generates a velocity command in SE(2) from uniform distribution. @@ -65,7 +68,7 @@ def __init__(self, cfg: UniformVelocityCommandCfg, env: ManagerBasedEnv): " parameter is set to None." ) if self.cfg.ranges.heading and not self.cfg.heading_command: - omni.log.warn( + logger.warning( f"The velocity command has the 'ranges.heading' attribute set to '{self.cfg.ranges.heading}'" " but the heading command is not active. Consider setting the flag for the heading command to True." ) diff --git a/source/isaaclab/isaaclab/envs/mdp/curriculums.py b/source/isaaclab/isaaclab/envs/mdp/curriculums.py index d24df89d9f4b..8438e5bec925 100644 --- a/source/isaaclab/isaaclab/envs/mdp/curriculums.py +++ b/source/isaaclab/isaaclab/envs/mdp/curriculums.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -65,7 +65,9 @@ class modify_env_param(ManagerTermBase): .. code-block:: python def modify_fn(env, env_ids, old_value, **modify_params) -> new_value | modify_env_param.NO_CHANGE: - ... + # modify the value based on the old value and the modify parameters + new_value = old_value + modify_params["value"] + return new_value where ``env`` is the learning environment, ``env_ids`` are the sub-environment indices, ``old_value`` is the current value of the target attribute, and ``modify_params`` @@ -78,10 +80,10 @@ def modify_fn(env, env_ids, old_value, **modify_params) -> new_value | modify_en current value, and the setter writes a new value back to the attribute. This term processes getter/setter accessors for a target attribute in an(specified by - as an "address" in the term configuration`cfg.params["address"]`) the first time it is called, then on each invocation - reads the current value, applies a user-provided `modify_fn`, and writes back - the result. Since None in this case can sometime be desirable value to write, we - use token, NO_CHANGE, as non-modification signal to this class, see usage below. + as an "address" in the term configuration :attr:`cfg.params["address"]`) the first time it is called, + then on each invocation reads the current value, applies a user-provided :attr:`modify_fn`, + and writes back the result. Since :obj:`None` in this case can sometime be desirable value + to write, we use token, :attr:`NO_CHANGE`, as non-modification signal to this class, see usage below. Usage: .. code-block:: python @@ -101,6 +103,7 @@ def resample_bucket_range( # to the setter being called, which may add overhead. return mdp.modify_env_param.NO_CHANGE + object_physics_material_curriculum = CurrTerm( func=mdp.modify_env_param, params={ @@ -110,9 +113,9 @@ def resample_bucket_range( "static_friction_range": [0.5, 1.0], "dynamic_friction_range": [0.3, 1.0], "restitution_range": [0.0, 0.5], - "num_step": 120000 - } - } + "num_step": 120000, + }, + }, ) """ @@ -275,13 +278,14 @@ def override_value(env, env_ids, data, value, num_steps): return value return mdp.modify_term_cfg.NO_CHANGE + command_object_pose_xrange_adr = CurrTerm( func=mdp.modify_term_cfg, params={ - "address": "commands.object_pose.ranges.pos_x", # note: `_manager.cfg` is omitted + "address": "commands.object_pose.ranges.pos_x", # note: `_manager.cfg` is omitted "modify_fn": override_value, - "modify_params": {"value": (-.75, -.25), "num_steps": 12000} - } + "modify_params": {"value": (-0.75, -0.25), "num_steps": 12000}, + }, ) """ diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py index 923fd1597abb..484121f4e491 100644 --- a/source/isaaclab/isaaclab/envs/mdp/events.py +++ b/source/isaaclab/isaaclab/envs/mdp/events.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,15 +14,16 @@ from __future__ import annotations +import logging import math import re -import torch from typing import TYPE_CHECKING, Literal +import torch + import carb import omni.physics.tensors.impl.api as physx from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.stage import get_current_stage from pxr import Gf, Sdf, UsdGeom, Vt import isaaclab.sim as sim_utils @@ -30,12 +31,16 @@ from isaaclab.actuators import ImplicitActuator from isaaclab.assets import Articulation, DeformableObject, RigidObject from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.terrains import TerrainImporter -from isaaclab.utils.version import compare_versions +from isaaclab.utils.version import compare_versions, get_isaac_sim_version if TYPE_CHECKING: from isaaclab.envs import ManagerBasedEnv +# import logger +logger = logging.getLogger(__name__) + def randomize_rigid_body_scale( env: ManagerBasedEnv, @@ -281,13 +286,14 @@ def __call__( class randomize_rigid_body_mass(ManagerTermBase): """Randomize the mass of the bodies by adding, scaling, or setting random values. - This function allows randomizing the mass of the bodies of the asset. The function samples random values from the - given distribution parameters and adds, scales, or sets the values into the physics simulation based on the operation. + This function allows randomizing the mass of the bodies of the asset. The function samples random + values from the given distribution parameters and adds, scales, or sets the values into the physics + simulation based on the operation. - If the ``recompute_inertia`` flag is set to ``True``, the function recomputes the inertia tensor of the bodies - after setting the mass. This is useful when the mass is changed significantly, as the inertia tensor depends - on the mass. It assumes the body is a uniform density object. If the body is not a uniform density object, - the inertia tensor may not be accurate. + If the :attr:`recompute_inertia` flag is set to :obj:`True`, the function recomputes the inertia tensor + of the bodies after setting the mass. This is useful when the mass is changed significantly, as the + inertia tensor depends on the mass. It assumes the body is a uniform density object. If the body is not + a uniform density object, the inertia tensor may not be accurate. .. tip:: This function uses CPU tensors to assign the body masses. It is recommended to use this function @@ -323,6 +329,12 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): "Randomization term 'randomize_rigid_body_mass' does not support operation:" f" '{cfg.params['operation']}'." ) + if cfg.params.get("min_mass") is not None: + if cfg.params.get("min_mass") < 1e-6: + raise ValueError( + "Randomization term 'randomize_rigid_body_mass' does not support 'min_mass' less than 1e-6 to avoid" + " physics errors." + ) def __call__( self, @@ -333,6 +345,7 @@ def __call__( operation: Literal["add", "scale", "abs"], distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", recompute_inertia: bool = True, + min_mass: float = 1e-6, ): # resolve environment ids if env_ids is None: @@ -360,6 +373,7 @@ def __call__( masses = _randomize_prop_by_op( masses, mass_distribution_params, env_ids, body_ids, operation=operation, distribution=distribution ) + masses = torch.clamp(masses, min=min_mass) # ensure masses are positive # set the mass into the physics simulation self.asset.root_physx_view.set_masses(masses, env_ids) @@ -529,9 +543,9 @@ class randomize_actuator_gains(ManagerTermBase): This function allows randomizing the actuator stiffness and damping gains. - The function samples random values from the given distribution parameters and applies the operation to the joint properties. - It then sets the values into the actuator models. If the distribution parameters are not provided for a particular property, - the function does not modify the property. + The function samples random values from the given distribution parameters and applies the operation to + the joint properties. It then sets the values into the actuator models. If the distribution parameters + are not provided for a particular property, the function does not modify the property. .. tip:: For implicit actuators, this function uses CPU tensors to assign the actuator gains into the simulation. @@ -703,6 +717,11 @@ def __call__( else: joint_ids = torch.tensor(self.asset_cfg.joint_ids, dtype=torch.int, device=self.asset.device) + if env_ids != slice(None) and joint_ids != slice(None): + env_ids_for_slice = env_ids[:, None] + else: + env_ids_for_slice = env_ids + # sample joint properties from the given ranges and set into the physics simulation # joint friction coefficient if friction_distribution_params is not None: @@ -719,11 +738,10 @@ def __call__( friction_coeff = torch.clamp(friction_coeff, min=0.0) # Always set static friction (indexed once) - static_friction_coeff = friction_coeff[env_ids[:, None], joint_ids] + static_friction_coeff = friction_coeff[env_ids_for_slice, joint_ids] # if isaacsim version is lower than 5.0.0 we can set only the static friction coefficient - major_version = int(env.sim.get_version()[0]) - if major_version >= 5: + if get_isaac_sim_version().major >= 5: # Randomize raw tensors dynamic_friction_coeff = _randomize_prop_by_op( self.asset.data.default_joint_dynamic_friction_coeff.clone(), @@ -750,8 +768,8 @@ def __call__( dynamic_friction_coeff = torch.minimum(dynamic_friction_coeff, friction_coeff) # Index once at the end - dynamic_friction_coeff = dynamic_friction_coeff[env_ids[:, None], joint_ids] - viscous_friction_coeff = viscous_friction_coeff[env_ids[:, None], joint_ids] + dynamic_friction_coeff = dynamic_friction_coeff[env_ids_for_slice, joint_ids] + viscous_friction_coeff = viscous_friction_coeff[env_ids_for_slice, joint_ids] else: # For versions < 5.0.0, we do not set these values dynamic_friction_coeff = None @@ -777,7 +795,7 @@ def __call__( distribution=distribution, ) self.asset.write_joint_armature_to_sim( - armature[env_ids[:, None], joint_ids], joint_ids=joint_ids, env_ids=env_ids + armature[env_ids_for_slice, joint_ids], joint_ids=joint_ids, env_ids=env_ids ) # joint position limits @@ -805,7 +823,7 @@ def __call__( ) # extract the position limits for the concerned joints - joint_pos_limits = joint_pos_limits[env_ids[:, None], joint_ids] + joint_pos_limits = joint_pos_limits[env_ids_for_slice, joint_ids] if (joint_pos_limits[..., 0] > joint_pos_limits[..., 1]).any(): raise ValueError( "Randomization term 'randomize_joint_parameters' is setting lower joint limits that are greater" @@ -823,10 +841,9 @@ class randomize_fixed_tendon_parameters(ManagerTermBase): This function allows randomizing the fixed tendon parameters of the asset. These correspond to the physics engine tendon properties that affect the joint behavior. - The function samples random values from the given distribution parameters and applies the operation to the tendon properties. - It then sets the values into the physics simulation. If the distribution parameters are not provided for a - particular property, the function does not modify the property. - + The function samples random values from the given distribution parameters and applies the operation to + the tendon properties. It then sets the values into the physics simulation. If the distribution parameters + are not provided for a particular property, the function does not modify the property. """ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): @@ -1018,7 +1035,12 @@ def apply_external_force_torque( torques = math_utils.sample_uniform(*torque_range, size, asset.device) # set the forces and torques into the buffers # note: these are only applied when you call: `asset.write_data_to_sim()` - asset.set_external_force_and_torque(forces, torques, env_ids=env_ids, body_ids=asset_cfg.body_ids) + asset.permanent_wrench_composer.set_forces_and_torques( + forces=forces, + torques=torques, + body_ids=asset_cfg.body_ids, + env_ids=env_ids, + ) def push_by_setting_velocity( @@ -1449,7 +1471,7 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): # This pattern (e.g., /World/envs/env_.*/Table/.*) should match visual prims # whether they end in /visuals or have other structures. prim_path = f"{asset_main_prim_path}/.*" - carb.log_info( + logging.info( f"Pattern '{pattern_with_visuals}' found no prims. Falling back to '{prim_path}' for texture" " randomization." ) diff --git a/source/isaaclab/isaaclab/envs/mdp/observations.py b/source/isaaclab/isaaclab/envs/mdp/observations.py index ac502521aae0..9839e0d70837 100644 --- a/source/isaaclab/isaaclab/envs/mdp/observations.py +++ b/source/isaaclab/isaaclab/envs/mdp/observations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import SceneEntityCfg @@ -154,6 +155,8 @@ def body_pose_w( # access the body poses in world frame pose = asset.data.body_pose_w[:, asset_cfg.body_ids, :7] + if isinstance(asset_cfg.body_ids, (slice, int)): + pose = pose.clone() # if slice or int, make a copy to avoid modifying original data pose[..., :3] = pose[..., :3] - env.scene.env_origins.unsqueeze(1) return pose.reshape(env.num_envs, -1) diff --git a/source/isaaclab/isaaclab/envs/mdp/recorders/__init__.py b/source/isaaclab/isaaclab/envs/mdp/recorders/__init__.py index d2d0080c87c3..cacc5e15c7f5 100644 --- a/source/isaaclab/isaaclab/envs/mdp/recorders/__init__.py +++ b/source/isaaclab/isaaclab/envs/mdp/recorders/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/recorders/recorders.py b/source/isaaclab/isaaclab/envs/mdp/recorders/recorders.py index 18823bb0fa48..a9315bbca63d 100644 --- a/source/isaaclab/isaaclab/envs/mdp/recorders/recorders.py +++ b/source/isaaclab/isaaclab/envs/mdp/recorders/recorders.py @@ -1,12 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Sequence +import torch + from isaaclab.managers.recorder_manager import RecorderTerm diff --git a/source/isaaclab/isaaclab/envs/mdp/recorders/recorders_cfg.py b/source/isaaclab/isaaclab/envs/mdp/recorders/recorders_cfg.py index 4fb6476c973d..d67350d7f209 100644 --- a/source/isaaclab/isaaclab/envs/mdp/recorders/recorders_cfg.py +++ b/source/isaaclab/isaaclab/envs/mdp/recorders/recorders_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/mdp/rewards.py b/source/isaaclab/isaaclab/envs/mdp/rewards.py index 20df2a557eb7..774c37aefa91 100644 --- a/source/isaaclab/isaaclab/envs/mdp/rewards.py +++ b/source/isaaclab/isaaclab/envs/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import SceneEntityCfg from isaaclab.managers.manager_base import ManagerTermBase @@ -136,7 +137,9 @@ def body_lin_acc_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEnt def joint_torques_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize joint torques applied on the articulation using L2 squared kernel. - NOTE: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint torques contribute to the term. + .. note:: + Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint torques + contribute to the term. """ # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] @@ -153,7 +156,9 @@ def joint_vel_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Ten def joint_vel_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize joint velocities on the articulation using L2 squared kernel. - NOTE: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint velocities contribute to the term. + .. note:: + Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint velocities + contribute to the term. """ # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] @@ -163,7 +168,9 @@ def joint_vel_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntity def joint_acc_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize joint accelerations on the articulation using L2 squared kernel. - NOTE: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint accelerations contribute to the term. + .. note:: + Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint accelerations + contribute to the term. """ # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] diff --git a/source/isaaclab/isaaclab/envs/mdp/terminations.py b/source/isaaclab/isaaclab/envs/mdp/terminations.py index a13ce9bb4a2d..84c83b2a213e 100644 --- a/source/isaaclab/isaaclab/envs/mdp/terminations.py +++ b/source/isaaclab/isaaclab/envs/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import ContactSensor diff --git a/source/isaaclab/isaaclab/envs/mimic_env_cfg.py b/source/isaaclab/isaaclab/envs/mimic_env_cfg.py index 53b48de13e11..c506df7f20bf 100644 --- a/source/isaaclab/isaaclab/envs/mimic_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +9,10 @@ """ Base MimicEnvCfg object for Isaac Lab Mimic data generation. """ + import enum +from isaaclab.managers.recorder_manager import RecorderManagerBaseCfg from isaaclab.utils import configclass @@ -76,6 +78,9 @@ class DataGenConfig: use_skillgen: bool = False """Whether to use skillgen to generate motion trajectories.""" + use_navigation_controller: bool = False + """Whether to use a navigation controller to generate loco-manipulation trajectories.""" + @configclass class SubTaskConfig: @@ -211,7 +216,8 @@ def generate_runtime_subtask_constraints(self): - "coordination" For a "sequential" constraint: - - Data from task_constraint_configs is added to task_constraints_dict as "sequential former" task constraint. + - Data from task_constraint_configs is added to task_constraints_dict as "sequential former" + task constraint. - The opposite constraint, of type "sequential latter", is also added to task_constraints_dict. - Additionally, a ("fulfilled", Bool) key-value pair is added to task_constraints_dict. - This is used to check if the precondition (i.e., the sequential former task) has been met. @@ -223,7 +229,8 @@ def generate_runtime_subtask_constraints(self): - The opposite constraint, of type "coordination", is also added to task_constraints_dict. - The number of synchronous steps is set to the minimum of subtask_len and concurrent_subtask_len. - This ensures both concurrent tasks end at the same time step. - - A "selected_src_demo_ind" and "transform" field are used to ensure the transforms used by both subtasks are the same. + - A "selected_src_demo_ind" and "transform" field are used to ensure the transforms used by + both subtasks are the same. """ task_constraints_dict = dict() if self.constraint_type == SubTaskConstraintType.SEQUENTIAL: @@ -308,3 +315,6 @@ class MimicEnvCfg: # List of configurations for subtask constraints task_constraint_configs: list[SubTaskConstraintConfig] = [] + + # Optional recorder configuration + mimic_recorder_config: RecorderManagerBaseCfg | None = None diff --git a/source/isaaclab/isaaclab/envs/ui/__init__.py b/source/isaaclab/isaaclab/envs/ui/__init__.py index 4227ef7f4946..93db88399e45 100644 --- a/source/isaaclab/isaaclab/envs/ui/__init__.py +++ b/source/isaaclab/isaaclab/envs/ui/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/ui/base_env_window.py b/source/isaaclab/isaaclab/envs/ui/base_env_window.py index 6744238b5a95..2aafe5e6bba2 100644 --- a/source/isaaclab/isaaclab/envs/ui/base_env_window.py +++ b/source/isaaclab/isaaclab/envs/ui/base_env_window.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +15,9 @@ import omni.kit.app import omni.kit.commands import omni.usd -from isaacsim.core.utils.stage import get_current_stage from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.ui.widgets import ManagerLiveVisualizer if TYPE_CHECKING: diff --git a/source/isaaclab/isaaclab/envs/ui/empty_window.py b/source/isaaclab/isaaclab/envs/ui/empty_window.py index 8255b5b07929..bc12f862d1b0 100644 --- a/source/isaaclab/isaaclab/envs/ui/empty_window.py +++ b/source/isaaclab/isaaclab/envs/ui/empty_window.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/ui/manager_based_rl_env_window.py b/source/isaaclab/isaaclab/envs/ui/manager_based_rl_env_window.py index 9e4c7defd246..72f0be06f199 100644 --- a/source/isaaclab/isaaclab/envs/ui/manager_based_rl_env_window.py +++ b/source/isaaclab/isaaclab/envs/ui/manager_based_rl_env_window.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py b/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py index 94ecdbc24613..0ba5368734b0 100644 --- a/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py +++ b/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,12 +6,13 @@ from __future__ import annotations import copy -import numpy as np -import torch import weakref from collections.abc import Sequence from typing import TYPE_CHECKING +import numpy as np +import torch + import omni.kit.app import omni.timeline diff --git a/source/isaaclab/isaaclab/envs/utils/__init__.py b/source/isaaclab/isaaclab/envs/utils/__init__.py index 63da1e10ad2d..d28381b15b76 100644 --- a/source/isaaclab/isaaclab/envs/utils/__init__.py +++ b/source/isaaclab/isaaclab/envs/utils/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/envs/utils/io_descriptors.py b/source/isaaclab/isaaclab/envs/utils/io_descriptors.py index bb5290c56bbd..84b8a5cf8a0c 100644 --- a/source/isaaclab/isaaclab/envs/utils/io_descriptors.py +++ b/source/isaaclab/isaaclab/envs/utils/io_descriptors.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +11,11 @@ from isaaclab.utils import configclass if TYPE_CHECKING: - from isaaclab.envs import ManagerBasedEnv - from isaaclab.assets.articulation import Articulation import torch + from isaaclab.assets.articulation import Articulation + from isaaclab.envs import ManagerBasedEnv + import dataclasses import functools import inspect @@ -120,49 +121,70 @@ def generic_io_descriptor( on_inspect: Callable[..., Any] | list[Callable[..., Any]] | None = None, **descriptor_kwargs: Any, ) -> Callable[[Callable[Concatenate[ManagerBasedEnv, P], R]], Callable[Concatenate[ManagerBasedEnv, P], R]]: - """ - Decorator factory for generic IO descriptors. + """Decorator factory for generic IO descriptors. This decorator can be used in different ways: + 1. The default decorator has all the information I need for my use case: - ..code-block:: python - @generic_io_descriptor(GenericIODescriptor(description="..", dtype="..")) - def my_func(env: ManagerBasedEnv, *args, **kwargs): + + ..code-block:: python + @generic_io_descriptor(GenericIODescriptor(description="..", dtype="..")) + def my_func(env: ManagerBasedEnv, *args, **kwargs): ... - ..note:: If description is not set, the function's docstring is used to populate it. + + ..note:: If description is not set, the function's docstring is used to populate it. 2. I need to add more information to the descriptor: - ..code-block:: python - @generic_io_descriptor(description="..", new_var_1="a", new_var_2="b") - def my_func(env: ManagerBasedEnv, *args, **kwargs): - ... + + ..code-block:: python + @generic_io_descriptor(description="..", new_var_1="a", new_var_2="b") + def my_func(env: ManagerBasedEnv, *args, **kwargs): + ... + 3. I need to add a hook to the descriptor: - ..code-block:: python - def record_shape(tensor: torch.Tensor, desc: GenericIODescriptor, **kwargs): - desc.shape = (tensor.shape[-1],) + + ..code-block:: python + def record_shape(tensor: torch.Tensor, desc: GenericIODescriptor, **kwargs): + desc.shape = (tensor.shape[-1],) @generic_io_descriptor(description="..", new_var_1="a", new_var_2="b", on_inspect=[record_shape, record_dtype]) def my_func(env: ManagerBasedEnv, *args, **kwargs): - ..note:: The hook is called after the function is called, if and only if the `inspect` flag is set when calling the function. - - For example: - ..code-block:: python - my_func(env, inspect=True) - - 4. I need to add a hook to the descriptor and this hook will write to a variable that is not part of the base descriptor. - ..code-block:: python - def record_joint_names(output: torch.Tensor, descriptor: GenericIODescriptor, **kwargs): - asset: Articulation = kwargs["env"].scene[kwargs["asset_cfg"].name] - joint_ids = kwargs["asset_cfg"].joint_ids - if joint_ids == slice(None, None, None): + ... + + ..note:: + + The hook is called after the function is called, if and only if the `inspect` flag is set when + calling the function. + + For example: + + ..code-block:: python + my_func(env, inspect=True) + + 4. I need to add a hook to the descriptor and this hook will write to a variable that is not part of + the base descriptor. + + ..code-block:: python + + def record_joint_names(output: torch.Tensor, descriptor: GenericIODescriptor, **kwargs): + asset: Articulation = kwargs["env"].scene[kwargs["asset_cfg"].name] + joint_ids = kwargs["asset_cfg"].joint_ids + if joint_ids == slice(None, None, None): joint_ids = list(range(len(asset.joint_names))) - descriptor.joint_names = [asset.joint_names[i] for i in joint_ids] + descriptor.joint_names = [asset.joint_names[i] for i in joint_ids] - @generic_io_descriptor(new_var_1="a", new_var_2="b", on_inspect=[record_shape, record_dtype, record_joint_names]) - def my_func(env: ManagerBasedEnv, *args, **kwargs): + @generic_io_descriptor( + new_var_1="a", + new_var_2="b", + on_inspect=[record_shape, record_dtype, record_joint_names], + ) + def my_func(env: ManagerBasedEnv, *args, **kwargs): + ... + + ..note:: - ..note:: The hook can access all the variables in the wrapped function's signature. While it is useful, the user should be careful to - access only existing variables. + The hook can access all the variables in the wrapped function's signature. While it is useful, + the user should be careful to access only existing variables. Args: _func: The function to decorate. @@ -185,7 +207,6 @@ def my_func(env: ManagerBasedEnv, *args, **kwargs): inspect_hooks: list[Callable[..., Any]] = list(on_inspect or []) # handles None def _apply(func: Callable[Concatenate[ManagerBasedEnv, P], R]) -> Callable[Concatenate[ManagerBasedEnv, P], R]: - # Capture the signature of the function sig = inspect.signature(func) diff --git a/source/isaaclab/isaaclab/envs/utils/marl.py b/source/isaaclab/isaaclab/envs/utils/marl.py index 0821a67faddd..010f6e5e27bd 100644 --- a/source/isaaclab/isaaclab/envs/utils/marl.py +++ b/source/isaaclab/isaaclab/envs/utils/marl.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 gymnasium as gym import math +from typing import Any + +import gymnasium as gym import numpy as np import torch -from typing import Any from ..common import ActionType, AgentID, EnvStepReturn, ObsType, StateType, VecEnvObs, VecEnvStepReturn from ..direct_marl_env import DirectMARLEnv @@ -17,8 +18,8 @@ def multi_agent_to_single_agent(env: DirectMARLEnv, state_as_observation: bool = False) -> DirectRLEnv: """Convert the multi-agent environment instance to a single-agent environment instance. - The converted environment will be an instance of the single-agent environment interface class (:class:`DirectRLEnv`). - As part of the conversion process, the following operations are carried out: + The converted environment will be an instance of the single-agent environment interface class + (:class:`DirectRLEnv`). As part of the conversion process, the following operations are carried out: * The observations of all the agents in the original multi-agent environment are concatenated to compose the single-agent observation. If the use of the environment state is defined as the observation, diff --git a/source/isaaclab/isaaclab/envs/utils/spaces.py b/source/isaaclab/isaaclab/envs/utils/spaces.py index 543c9902a8f7..a21ecd82667c 100644 --- a/source/isaaclab/isaaclab/envs/utils/spaces.py +++ b/source/isaaclab/isaaclab/envs/utils/spaces.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 gymnasium as gym import json +from typing import Any + +import gymnasium as gym import numpy as np import torch -from typing import Any from ..common import SpaceType @@ -54,14 +55,16 @@ def sample_space(space: gym.spaces.Space, device: str, batch_size: int = -1, fil Args: space: Gymnasium space. device: The device where the tensor should be created. - batch_size: Batch size. If the specified value is greater than zero, a batched space will be created and sampled from it. - fill_value: The value to fill the created tensors with. If None (default value), tensors will keep their random values. + batch_size: Batch size. If the specified value is greater than zero, a batched space + will be created and sampled from it. + fill_value: The value to fill the created tensors with. If None (default value), tensors + will keep their random values. Returns: Tensorized sampled space. """ - def tensorize(s, x): + def tensorize(s: gym.spaces.Space, x: Any) -> Any: if isinstance(s, gym.spaces.Box): tensor = torch.tensor(x, device=device, dtype=torch.float32).reshape(batch_size, *s.shape) if fill_value is not None: @@ -89,6 +92,9 @@ def tensorize(s, x): elif isinstance(s, gym.spaces.Tuple): return tuple([tensorize(_s, v) for _s, v in zip(s, x)]) + # If the space is not supported, raise an error + raise ValueError(f"Unsupported Gymnasium space for tensorization: {s}") + sample = (gym.vector.utils.batch_space(space, batch_size) if batch_size > 0 else space).sample() return tensorize(space, sample) @@ -106,13 +112,15 @@ def serialize_space(space: SpaceType) -> str: if isinstance(space, gym.spaces.Discrete): return json.dumps({"type": "gymnasium", "space": "Discrete", "n": int(space.n)}) elif isinstance(space, gym.spaces.Box): - return json.dumps({ - "type": "gymnasium", - "space": "Box", - "low": space.low.tolist(), - "high": space.high.tolist(), - "shape": space.shape, - }) + return json.dumps( + { + "type": "gymnasium", + "space": "Box", + "low": space.low.tolist(), + "high": space.high.tolist(), + "shape": space.shape, + } + ) elif isinstance(space, gym.spaces.MultiDiscrete): return json.dumps({"type": "gymnasium", "space": "MultiDiscrete", "nvec": space.nvec.tolist()}) elif isinstance(space, gym.spaces.Tuple): diff --git a/source/isaaclab/isaaclab/managers/__init__.py b/source/isaaclab/isaaclab/managers/__init__.py index ee83bf677577..4a8266801b47 100644 --- a/source/isaaclab/isaaclab/managers/__init__.py +++ b/source/isaaclab/isaaclab/managers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/managers/action_manager.py b/source/isaaclab/isaaclab/managers/action_manager.py index 9b561ceb6a70..6d138451f998 100644 --- a/source/isaaclab/isaaclab/managers/action_manager.py +++ b/source/isaaclab/isaaclab/managers/action_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,22 +9,23 @@ import inspect import re -import torch import weakref from abc import abstractmethod from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING, Any +import torch +from prettytable import PrettyTable + import omni.kit.app -from isaaclab.assets import AssetBase from isaaclab.envs.utils.io_descriptors import GenericActionIODescriptor from .manager_base import ManagerBase, ManagerTermBase from .manager_term_cfg import ActionTermCfg if TYPE_CHECKING: + from isaaclab.assets import AssetBase from isaaclab.envs import ManagerBasedEnv diff --git a/source/isaaclab/isaaclab/managers/command_manager.py b/source/isaaclab/isaaclab/managers/command_manager.py index 330e348b570f..0fe66ff6b963 100644 --- a/source/isaaclab/isaaclab/managers/command_manager.py +++ b/source/isaaclab/isaaclab/managers/command_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,13 +8,14 @@ from __future__ import annotations import inspect -import torch import weakref from abc import abstractmethod from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING +import torch +from prettytable import PrettyTable + import omni.kit.app from .manager_base import ManagerBase, ManagerTermBase diff --git a/source/isaaclab/isaaclab/managers/curriculum_manager.py b/source/isaaclab/isaaclab/managers/curriculum_manager.py index 2c6fa3fbeb8e..5354641d9e7e 100644 --- a/source/isaaclab/isaaclab/managers/curriculum_manager.py +++ b/source/isaaclab/isaaclab/managers/curriculum_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,12 @@ from __future__ import annotations -import torch from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING +import torch +from prettytable import PrettyTable + from .manager_base import ManagerBase, ManagerTermBase from .manager_term_cfg import CurriculumTermCfg diff --git a/source/isaaclab/isaaclab/managers/event_manager.py b/source/isaaclab/isaaclab/managers/event_manager.py index 639be925e0a7..8f92d6859c1e 100644 --- a/source/isaaclab/isaaclab/managers/event_manager.py +++ b/source/isaaclab/isaaclab/managers/event_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,12 +8,12 @@ from __future__ import annotations import inspect -import torch +import logging from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING -import omni.log +import torch +from prettytable import PrettyTable from .manager_base import ManagerBase from .manager_term_cfg import EventTermCfg @@ -21,6 +21,9 @@ if TYPE_CHECKING: from isaaclab.envs import ManagerBasedEnv +# import logger +logger = logging.getLogger(__name__) + class EventManager(ManagerBase): """Manager for orchestrating operations based on different simulation events. @@ -185,7 +188,7 @@ def apply( """ # check if mode is valid if mode not in self._mode_term_names: - omni.log.warn(f"Event mode '{mode}' is not defined. Skipping event.") + logger.warning(f"Event mode '{mode}' is not defined. Skipping event.") return # check if mode is interval and dt is not provided @@ -348,7 +351,7 @@ def _prepare_terms(self): ) if term_cfg.mode != "reset" and term_cfg.min_step_count_between_reset != 0: - omni.log.warn( + logger.warning( f"Event term '{term_name}' has 'min_step_count_between_reset' set to a non-zero value" " but the mode is not 'reset'. Ignoring the 'min_step_count_between_reset' value." ) @@ -370,7 +373,7 @@ def _prepare_terms(self): # can be initialized before the simulation starts. # this is done to ensure that the USD-level randomization is possible before the simulation starts. if inspect.isclass(term_cfg.func) and term_cfg.mode == "prestartup": - omni.log.info(f"Initializing term '{term_name}' with class '{term_cfg.func.__name__}'.") + logger.info(f"Initializing term '{term_name}' with class '{term_cfg.func.__name__}'.") term_cfg.func = term_cfg.func(cfg=term_cfg, env=self._env) # check if mode is a new mode diff --git a/source/isaaclab/isaaclab/managers/manager_base.py b/source/isaaclab/isaaclab/managers/manager_base.py index 081c3e271eca..158c713abfa3 100644 --- a/source/isaaclab/isaaclab/managers/manager_base.py +++ b/source/isaaclab/isaaclab/managers/manager_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ import copy import inspect +import logging import weakref from abc import ABC, abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING, Any -import omni.log import omni.timeline import isaaclab.utils.string as string_utils @@ -24,6 +24,9 @@ if TYPE_CHECKING: from isaaclab.envs import ManagerBasedEnv +# import logger +logger = logging.getLogger(__name__) + class ManagerTermBase(ABC): """Base class for manager terms. @@ -42,13 +45,14 @@ class should also have a corresponding configuration class that defines the conf from isaaclab.utils import configclass from isaaclab.utils.mdp import ManagerBase, ManagerTermBaseCfg + @configclass class MyManagerCfg: - my_term_1: ManagerTermBaseCfg = ManagerTermBaseCfg(...) my_term_2: ManagerTermBaseCfg = ManagerTermBaseCfg(...) my_term_3: ManagerTermBaseCfg = ManagerTermBaseCfg(...) + # define manager instance my_manager = ManagerBase(cfg=ManagerCfg(), env=env) @@ -404,11 +408,11 @@ def _process_term_cfg_at_play(self, term_name: str, term_cfg: ManagerTermBaseCfg if value.body_ids is not None: msg += f"\n\tBody names: {value.body_names} [{value.body_ids}]" # print the information - omni.log.info(msg) + logger.info(msg) # store the entity term_cfg.params[key] = value # initialize the term if it is a class if inspect.isclass(term_cfg.func): - omni.log.info(f"Initializing term '{term_name}' with class '{term_cfg.func.__name__}'.") + logger.info(f"Initializing term '{term_name}' with class '{term_cfg.func.__name__}'.") term_cfg.func = term_cfg.func(cfg=term_cfg, env=self._env) diff --git a/source/isaaclab/isaaclab/managers/manager_term_cfg.py b/source/isaaclab/isaaclab/managers/manager_term_cfg.py index 137d91aae59f..de7c23aa220b 100644 --- a/source/isaaclab/isaaclab/managers/manager_term_cfg.py +++ b/source/isaaclab/isaaclab/managers/manager_term_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,12 @@ from __future__ import annotations -import torch from collections.abc import Callable from dataclasses import MISSING from typing import TYPE_CHECKING, Any +import torch + from isaaclab.utils import configclass from isaaclab.utils.modifiers import ModifierCfg from isaaclab.utils.noise import NoiseCfg, NoiseModelCfg @@ -64,7 +65,7 @@ class RecorderTermCfg: class_type: type[RecorderTerm] = MISSING """The associated recorder term class. - The class should inherit from :class:`isaaclab.managers.action_manager.RecorderTerm`. + The class should inherit from :class:`isaaclab.managers.recorder_manager.RecorderTerm`. """ @@ -183,14 +184,14 @@ class ObservationTermCfg(ManagerTermBaseCfg): history_length: int = 0 """Number of past observations to store in the observation buffers. Defaults to 0, meaning no history. - Observation history initializes to empty, but is filled with the first append after reset or initialization. Subsequent history - only adds a single entry to the history buffer. If flatten_history_dim is set to True, the source data of shape - (N, H, D, ...) where N is the batch dimension and H is the history length will be reshaped to a 2D tensor of shape - (N, H*D*...). Otherwise, the data will be returned as is. + Observation history initializes to empty, but is filled with the first append after reset or initialization. + Subsequent history only adds a single entry to the history buffer. If flatten_history_dim is set to True, + the source data of shape (N, H, D, ...) where N is the batch dimension and H is the history length will + be reshaped to a 2-D tensor of shape (N, H*D*...). Otherwise, the data will be returned as is. """ flatten_history_dim: bool = True - """Whether or not the observation manager should flatten history-based observation terms to a 2D (N, D) tensor. + """Whether or not the observation manager should flatten history-based observation terms to a 2-D (N, D) tensor. Defaults to True.""" @@ -201,8 +202,8 @@ class ObservationGroupCfg: concatenate_terms: bool = True """Whether to concatenate the observation terms in the group. Defaults to True. - If true, the observation terms in the group are concatenated along the dimension specified through :attr:`concatenate_dim`. - Otherwise, they are kept separate and returned as a dictionary. + If true, the observation terms in the group are concatenated along the dimension specified through + :attr:`concatenate_dim`. Otherwise, they are kept separate and returned as a dictionary. If the observation group contains terms of different dimensions, it must be set to False. """ @@ -211,10 +212,10 @@ class ObservationGroupCfg: """Dimension along to concatenate the different observation terms. Defaults to -1, which means the last dimension of the observation terms. - If :attr:`concatenate_terms` is True, this parameter specifies the dimension along which the observation terms are concatenated. - The indicated dimension depends on the shape of the observations. For instance, for a 2D RGB image of shape (H, W, C), the dimension - 0 means concatenating along the height, 1 along the width, and 2 along the channels. The offset due - to the batched environment is handled automatically. + If :attr:`concatenate_terms` is True, this parameter specifies the dimension along which the observation + terms are concatenated. The indicated dimension depends on the shape of the observations. For instance, + for a 2-D RGB image of shape (H, W, C), the dimension 0 means concatenating along the height, 1 along the + width, and 2 along the channels. The offset due to the batched environment is handled automatically. """ enable_corruption: bool = False @@ -227,13 +228,13 @@ class ObservationGroupCfg: history_length: int | None = None """Number of past observation to store in the observation buffers for all observation terms in group. - This parameter will override :attr:`ObservationTermCfg.history_length` if set. Defaults to None. If None, each - terms history will be controlled on a per term basis. See :class:`ObservationTermCfg` for details on history_length - implementation. + This parameter will override :attr:`ObservationTermCfg.history_length` if set. Defaults to None. + If None, each terms history will be controlled on a per term basis. See :class:`ObservationTermCfg` + for details on :attr:`ObservationTermCfg.history_length` implementation. """ flatten_history_dim: bool = True - """Flag to flatten history-based observation terms to a 2D (num_env, D) tensor for all observation terms in group. + """Flag to flatten history-based observation terms to a 2-D (num_env, D) tensor for all observation terms in group. Defaults to True. This parameter will override all :attr:`ObservationTermCfg.flatten_history_dim` in the group if diff --git a/source/isaaclab/isaaclab/managers/observation_manager.py b/source/isaaclab/isaaclab/managers/observation_manager.py index 014ae2a00b8f..a1bde0266f4b 100644 --- a/source/isaaclab/isaaclab/managers/observation_manager.py +++ b/source/isaaclab/isaaclab/managers/observation_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +8,12 @@ from __future__ import annotations import inspect +from collections.abc import Sequence +from typing import TYPE_CHECKING + import numpy as np import torch -from collections.abc import Sequence from prettytable import PrettyTable -from typing import TYPE_CHECKING from isaaclab.utils import class_to_dict, modifiers, noise from isaaclab.utils.buffers import CircularBuffer @@ -167,16 +168,20 @@ def get_active_iterable_terms(self, env_idx: int) -> Sequence[tuple[str, Sequenc continue idx = 0 + concat_dim = self._group_obs_concatenate_dim[group_name] + # handle cases where concat dim is positive, account for the batch dimension + if concat_dim > 0: + concat_dim -= 1 # add info for each term data = obs_buffer[group_name] for name, shape in zip( self._group_obs_term_names[group_name], self._group_obs_term_dim[group_name], ): - data_length = np.prod(shape) - term = data[env_idx, idx : idx + data_length] + # extract the term from the buffer based on the shape + term = data[env_idx].narrow(dim=concat_dim, start=idx, length=shape[concat_dim]) terms.append((group_name + "-" + name, term.cpu().tolist())) - idx += data_length + idx += shape[concat_dim] return terms @@ -501,19 +506,23 @@ def _prepare_terms(self): self._group_obs_term_dim[group_name] = list() self._group_obs_term_cfgs[group_name] = list() self._group_obs_class_term_cfgs[group_name] = list() + + # history buffers group_entry_history_buffer: dict[str, CircularBuffer] = dict() + # read common config for the group self._group_obs_concatenate[group_name] = group_cfg.concatenate_terms self._group_obs_concatenate_dim[group_name] = ( group_cfg.concatenate_dim + 1 if group_cfg.concatenate_dim >= 0 else group_cfg.concatenate_dim ) + # check if config is dict already if isinstance(group_cfg, dict): - group_cfg_items = group_cfg.items() + term_cfg_items = group_cfg.items() else: - group_cfg_items = group_cfg.__dict__.items() + term_cfg_items = group_cfg.__dict__.items() # iterate over all the terms in each group - for term_name, term_cfg in group_cfg_items: + for term_name, term_cfg in term_cfg_items: # skip non-obs settings if term_name in [ "enable_corruption", diff --git a/source/isaaclab/isaaclab/managers/recorder_manager.py b/source/isaaclab/isaaclab/managers/recorder_manager.py index 855c975f2a91..51a3f3e83518 100644 --- a/source/isaaclab/isaaclab/managers/recorder_manager.py +++ b/source/isaaclab/isaaclab/managers/recorder_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +8,12 @@ import enum import os -import torch from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING +import torch +from prettytable import PrettyTable + from isaaclab.utils import configclass from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler @@ -50,6 +51,9 @@ class RecorderManagerBaseCfg: export_in_record_pre_reset: bool = True """Whether to export episodes in the record_pre_reset call.""" + export_in_close: bool = False + """Whether to export episodes in the close call.""" + class RecorderTerm(ManagerTermBase): """Base class for recorder terms. @@ -132,6 +136,17 @@ def record_post_physics_decimation_step(self) -> tuple[str | None, torch.Tensor """ return None, None + def close(self, file_path: str): + """Finalize and "clean up" the recorder term. + + This can include tasks such as appending metadata (e.g. labels) to a file + and properly closing any associated file handles or resources. + + Args: + file_path: the absolute path to the file + """ + pass + class RecorderManager(ManagerBase): """Manager for recording data from recorder terms.""" @@ -202,15 +217,7 @@ def __str__(self) -> str: def __del__(self): """Destructor for recorder.""" - # Do nothing if no active recorder terms are provided - if len(self.active_terms) == 0: - return - - if self._dataset_file_handler is not None: - self._dataset_file_handler.close() - - if self._failed_episode_dataset_file_handler is not None: - self._failed_episode_dataset_file_handler.close() + self.close() """ Properties. @@ -442,12 +449,16 @@ def get_ep_meta(self) -> dict: ep_meta = self._env.cfg.get_ep_meta() return ep_meta - def export_episodes(self, env_ids: Sequence[int] | None = None) -> None: + def export_episodes(self, env_ids: Sequence[int] | None = None, demo_ids: Sequence[int] | None = None) -> None: """Concludes and exports the episodes for the given environment ids. Args: env_ids: The environment ids. Defaults to None, in which case all environments are considered. + demo_ids: Custom identifiers for the exported episodes. + If provided, episodes will be named "demo_{demo_id}" in the dataset. + Should have the same length as env_ids if both are provided. + If None, uses the default sequential naming scheme. Defaults to None. """ # Do nothing if no active recorder terms are provided if len(self.active_terms) == 0: @@ -458,6 +469,17 @@ def export_episodes(self, env_ids: Sequence[int] | None = None) -> None: if isinstance(env_ids, torch.Tensor): env_ids = env_ids.tolist() + # Handle demo_ids processing + if demo_ids is not None: + if isinstance(demo_ids, torch.Tensor): + demo_ids = demo_ids.tolist() + if len(demo_ids) != len(env_ids): + raise ValueError(f"Length of demo_ids ({len(demo_ids)}) must match length of env_ids ({len(env_ids)})") + # Check for duplicate demo_ids + if len(set(demo_ids)) != len(demo_ids): + duplicates = [x for i, x in enumerate(demo_ids) if demo_ids.index(x) != i] + raise ValueError(f"demo_ids must be unique. Found duplicates: {list(set(duplicates))}") + # Export episode data through dataset exporter need_to_flush = False @@ -468,7 +490,7 @@ def export_episodes(self, env_ids: Sequence[int] | None = None) -> None: if self._failed_episode_dataset_file_handler is not None: self._failed_episode_dataset_file_handler.add_env_args(ep_meta) - for env_id in env_ids: + for i, env_id in enumerate(env_ids): if env_id in self._episodes and not self._episodes[env_id].is_empty(): self._episodes[env_id].pre_export() @@ -484,7 +506,9 @@ def export_episodes(self, env_ids: Sequence[int] | None = None) -> None: else: target_dataset_file_handler = self._failed_episode_dataset_file_handler if target_dataset_file_handler is not None: - target_dataset_file_handler.write_episode(self._episodes[env_id]) + # Use corresponding demo_id if provided, otherwise None + current_demo_id = demo_ids[i] if demo_ids is not None else None + target_dataset_file_handler.write_episode(self._episodes[env_id], current_demo_id) need_to_flush = True # Update episode count if episode_succeeded: @@ -502,6 +526,22 @@ def export_episodes(self, env_ids: Sequence[int] | None = None) -> None: if self._failed_episode_dataset_file_handler is not None: self._failed_episode_dataset_file_handler.flush() + def close(self): + """Closes the recorder manager by exporting any remaining data to file as well as properly + closes the recorder terms. + """ + # Do nothing if no active recorder terms are provided + if len(self.active_terms) == 0: + return + if self._dataset_file_handler is not None: + if self.cfg.export_in_close: + self.export_episodes() + self._dataset_file_handler.close() + if self._failed_episode_dataset_file_handler is not None: + self._failed_episode_dataset_file_handler.close() + for term in self._terms.values(): + term.close(os.path.join(self.cfg.dataset_export_dir_path, self.cfg.dataset_filename)) + """ Helper functions. """ @@ -521,6 +561,7 @@ def _prepare_terms(self): "dataset_export_dir_path", "dataset_export_mode", "export_in_record_pre_reset", + "export_in_close", ]: continue # check if term config is None diff --git a/source/isaaclab/isaaclab/managers/reward_manager.py b/source/isaaclab/isaaclab/managers/reward_manager.py index 63077eacc4d0..d9c66a100a72 100644 --- a/source/isaaclab/isaaclab/managers/reward_manager.py +++ b/source/isaaclab/isaaclab/managers/reward_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,12 @@ from __future__ import annotations -import torch from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING +import torch +from prettytable import PrettyTable + from .manager_base import ManagerBase, ManagerTermBase from .manager_term_cfg import RewardTermCfg diff --git a/source/isaaclab/isaaclab/managers/scene_entity_cfg.py b/source/isaaclab/isaaclab/managers/scene_entity_cfg.py index 9e981fcbf792..f3b01f7eee0d 100644 --- a/source/isaaclab/isaaclab/managers/scene_entity_cfg.py +++ b/source/isaaclab/isaaclab/managers/scene_entity_cfg.py @@ -1,16 +1,21 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Configuration terms for different managers.""" +from __future__ import annotations + from dataclasses import MISSING +from typing import TYPE_CHECKING -from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection -from isaaclab.scene import InteractiveScene from isaaclab.utils import configclass +if TYPE_CHECKING: + from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection + from isaaclab.scene import InteractiveScene + @configclass class SceneEntityCfg: @@ -105,7 +110,8 @@ class for more details. For more details, see the :meth:`isaaclab.utils.string.resolve_matching_names` function. .. note:: - This attribute is only used when :attr:`joint_names`, :attr:`body_names`, or :attr:`object_collection_names` are specified. + This attribute is only used when :attr:`joint_names`, :attr:`body_names`, or :attr:`object_collection_names` + are specified. """ @@ -124,7 +130,8 @@ def resolve(self, scene: InteractiveScene): ValueError: If both ``joint_names`` and ``joint_ids`` are specified and are not consistent. ValueError: If both ``fixed_tendon_names`` and ``fixed_tendon_ids`` are specified and are not consistent. ValueError: If both ``body_names`` and ``body_ids`` are specified and are not consistent. - ValueError: If both ``object_collection_names`` and ``object_collection_ids`` are specified and are not consistent. + ValueError: If both ``object_collection_names`` and ``object_collection_ids`` are specified and + are not consistent. """ # check if the entity is valid if self.name not in scene.keys(): diff --git a/source/isaaclab/isaaclab/managers/termination_manager.py b/source/isaaclab/isaaclab/managers/termination_manager.py index 2c732b463638..0a557df628a9 100644 --- a/source/isaaclab/isaaclab/managers/termination_manager.py +++ b/source/isaaclab/isaaclab/managers/termination_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,12 @@ from __future__ import annotations -import torch from collections.abc import Sequence -from prettytable import PrettyTable from typing import TYPE_CHECKING +import torch +from prettytable import PrettyTable + from .manager_base import ManagerBase, ManagerTermBase from .manager_term_cfg import TerminationTermCfg @@ -63,6 +64,8 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): self._term_name_to_term_idx = {name: i for i, name in enumerate(self._term_names)} # prepare extra info to store individual termination term information self._term_dones = torch.zeros((self.num_envs, len(self._term_names)), device=self.device, dtype=torch.bool) + # prepare extra info to store last episode done per termination term information + self._last_episode_dones = torch.zeros_like(self._term_dones) # create buffer for managing termination per environment self._truncated_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) self._terminated_buf = torch.zeros_like(self._truncated_buf) @@ -138,7 +141,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor] env_ids = slice(None) # add to episode dict extras = {} - last_episode_done_stats = self._term_dones.float().mean(dim=0) + last_episode_done_stats = self._last_episode_dones.float().mean(dim=0) for i, key in enumerate(self._term_names): # store information extras["Episode_Termination/" + key] = last_episode_done_stats[i].item() @@ -169,15 +172,17 @@ def compute(self) -> torch.Tensor: else: self._terminated_buf |= value # add to episode dones - rows = value.nonzero(as_tuple=True)[0] # indexing is cheaper than boolean advance indexing - if rows.numel() > 0: - self._term_dones[rows] = False - self._term_dones[rows, i] = True + self._term_dones[:, i] = value + # update last-episode dones once per compute: for any env where a term fired, + # reflect exactly which term(s) fired this step and clear others + rows = self._term_dones.any(dim=1).nonzero(as_tuple=True)[0] + if rows.numel() > 0: + self._last_episode_dones[rows] = self._term_dones[rows] # return combined termination signal return self._truncated_buf | self._terminated_buf def get_term(self, name: str) -> torch.Tensor: - """Returns the termination term with the specified name. + """Returns the termination term value at current step with the specified name. Args: name: The name of the termination term. @@ -190,7 +195,8 @@ def get_term(self, name: str) -> torch.Tensor: def get_active_iterable_terms(self, env_idx: int) -> Sequence[tuple[str, Sequence[float]]]: """Returns the active terms as iterable sequence of tuples. - The first element of the tuple is the name of the term and the second element is the raw value(s) of the term. + The first element of the tuple is the name of the term and the second element is the raw value(s) of the term + recorded at current step. Args: env_idx: The specific environment to pull the active terms from. diff --git a/source/isaaclab/isaaclab/markers/__init__.py b/source/isaaclab/isaaclab/markers/__init__.py index 5e5b91591121..eb7b69761009 100644 --- a/source/isaaclab/isaaclab/markers/__init__.py +++ b/source/isaaclab/isaaclab/markers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/markers/config/__init__.py b/source/isaaclab/isaaclab/markers/config/__init__.py index 27f83022d31f..54d30051259a 100644 --- a/source/isaaclab/isaaclab/markers/config/__init__.py +++ b/source/isaaclab/isaaclab/markers/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -47,6 +47,15 @@ ) """Configuration for the deformable object's kinematic target marker.""" +VISUO_TACTILE_SENSOR_MARKER_CFG = VisualizationMarkersCfg( + markers={ + "tacsl_pts": sim_utils.SphereCfg( + radius=0.0002, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), + ), + }, +) +"""Configuration for the visuo-tactile sensor marker.""" ## # Frames. diff --git a/source/isaaclab/isaaclab/markers/visualization_markers.py b/source/isaaclab/isaaclab/markers/visualization_markers.py index ce4611289bcc..bd27009c0ec3 100644 --- a/source/isaaclab/isaaclab/markers/visualization_markers.py +++ b/source/isaaclab/isaaclab/markers/visualization_markers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -19,23 +19,23 @@ # needed to import for allowing type-hinting: np.ndarray | torch.Tensor | None from __future__ import annotations +import logging +from dataclasses import MISSING + import numpy as np import torch -from dataclasses import MISSING -import isaacsim.core.utils.stage as stage_utils -import omni.kit.commands -import omni.log import omni.physx.scripts.utils as physx_utils -from isaacsim.core.utils.stage import get_current_stage from pxr import Gf, PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, Vt import isaaclab.sim as sim_utils from isaaclab.sim.spawners import SpawnerCfg -from isaaclab.sim.utils import attach_stage_to_usd_context from isaaclab.utils.configclass import configclass from isaaclab.utils.math import convert_quat +# import logger +logger = logging.getLogger(__name__) + @configclass class VisualizationMarkersCfg: @@ -67,7 +67,7 @@ class VisualizationMarkers: The class parses the configuration to create different the marker prototypes into the stage. Each marker prototype prim is created as a child of the :class:`UsdGeom.PointInstancer` prim. The prim path for the marker prim is resolved using the key of the marker in the :attr:`VisualizationMarkersCfg.markers` - dictionary. The marker prototypes are created using the :meth:`isaacsim.core.utils.create_prim` + dictionary. The marker prototypes are created using the :meth:`isaaclab.sim.utils.prims.create_prim` function, and then instanced using :class:`UsdGeom.PointInstancer` prim to allow creating multiple instances of the marker prims. @@ -100,7 +100,7 @@ class VisualizationMarkers: radius=1.0, visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), ), - } + }, ) # Create the markers instance # This will create a UsdGeom.PointInstancer prim at the given path along with the marker prototypes. @@ -146,9 +146,9 @@ def __init__(self, cfg: VisualizationMarkersCfg): ValueError: When no markers are provided in the :obj:`cfg`. """ # get next free path for the prim - prim_path = stage_utils.get_next_free_path(cfg.prim_path) + prim_path = sim_utils.get_next_free_prim_path(cfg.prim_path) # create a new prim - self.stage = get_current_stage() + self.stage = sim_utils.get_current_stage() self._instancer_manager = UsdGeom.PointInstancer.Define(self.stage, prim_path) # store inputs self.prim_path = prim_path @@ -396,16 +396,11 @@ def _process_prototype_prim(self, prim: Usd.Prim): child_prim.SetInstanceable(False) # check if prim is a mesh -> if so, make it invisible to secondary rays if child_prim.IsA(UsdGeom.Gprim): - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "ChangePropertyCommand" kit command - attach_stage_to_usd_context(attaching_early=True) - # invisible to secondary rays such as depth images - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(f"{child_prim.GetPrimPath().pathString}.primvars:invisibleToSecondaryRays"), + sim_utils.change_prim_property( + prop_path=f"{child_prim.GetPrimPath().pathString}.primvars:invisibleToSecondaryRays", value=True, - prev=None, + stage=prim.GetStage(), type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool, ) # add children to list diff --git a/source/isaaclab/isaaclab/scene/__init__.py b/source/isaaclab/isaaclab/scene/__init__.py index 8a3e818559ad..4b614ea4bced 100644 --- a/source/isaaclab/isaaclab/scene/__init__.py +++ b/source/isaaclab/isaaclab/scene/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/scene/interactive_scene.py b/source/isaaclab/isaaclab/scene/interactive_scene.py index 15739c33ad7d..46e5895687aa 100644 --- a/source/isaaclab/isaaclab/scene/interactive_scene.py +++ b/source/isaaclab/isaaclab/scene/interactive_scene.py @@ -1,19 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch +import logging from collections.abc import Sequence from typing import Any +import torch + import carb -import omni.log -import omni.usd from isaacsim.core.cloner import GridCloner -from isaacsim.core.prims import XFormPrim -from isaacsim.core.utils.stage import get_current_stage -from isaacsim.core.version import get_version from pxr import PhysxSchema import isaaclab.sim as sim_utils @@ -32,11 +29,20 @@ ) from isaaclab.sensors import ContactSensorCfg, FrameTransformerCfg, SensorBase, SensorBaseCfg from isaaclab.sim import SimulationContext -from isaaclab.sim.utils import get_current_stage_id +from isaaclab.sim.utils.stage import get_current_stage, get_current_stage_id +from isaaclab.sim.views import XformPrimView from isaaclab.terrains import TerrainImporter, TerrainImporterCfg +from isaaclab.utils.version import get_isaac_sim_version + +# Note: This is a temporary import for the VisuoTactileSensorCfg class. +# It will be removed once the VisuoTactileSensor class is added to the core Isaac Lab framework. +from isaaclab_contrib.sensors.tacsl_sensor import VisuoTactileSensorCfg from .interactive_scene_cfg import InteractiveSceneCfg +# import logger +logger = logging.getLogger(__name__) + class InteractiveScene: """A scene that contains entities added to the simulation. @@ -71,9 +77,10 @@ class InteractiveScene: from isaaclab_assets.robots.anymal import ANYMAL_C_CFG + @configclass class MySceneCfg(InteractiveSceneCfg): - + # ANYmal-C robot spawned in each environment robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") Then the robot can be accessed from the scene as follows: @@ -144,8 +151,7 @@ def __init__(self, cfg: InteractiveSceneCfg): # this triggers per-object level cloning in the spawner. if not self.cfg.replicate_physics: # check version of Isaac Sim to determine whether clone_in_fabric is valid - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: # clone the env xform env_origins = self.cloner.clone( source_prim_path=self.env_prim_paths[0], @@ -184,8 +190,7 @@ def __init__(self, cfg: InteractiveSceneCfg): # this is done to make scene initialization faster at play time if self.cfg.replicate_physics and self.cfg.num_envs > 1: # check version of Isaac Sim to determine whether clone_in_fabric is valid - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: self.cloner.replicate_physics( source_prim_path=self.env_prim_paths[0], prim_paths=self.env_prim_paths, @@ -222,15 +227,14 @@ def clone_environments(self, copy_from_source: bool = False): carb_settings_iface = carb.settings.get_settings() has_multi_assets = carb_settings_iface.get("/isaaclab/spawn/multi_assets") if has_multi_assets and self.cfg.replicate_physics: - omni.log.warn( + logger.warning( "Varying assets might have been spawned under different environments." " However, the replicate physics flag is enabled in the 'InteractiveScene' configuration." " This may adversely affect PhysX parsing. We recommend disabling this property." ) # check version of Isaac Sim to determine whether clone_in_fabric is valid - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: # clone the environment env_origins = self.cloner.clone( source_prim_path=self.env_prim_paths[0], @@ -260,7 +264,7 @@ def clone_environments(self, copy_from_source: bool = False): if (not self.cfg.replicate_physics and self.cfg.filter_collisions) or self.device == "cpu": # if scene is specified through cfg, this is already taken care of if not self._is_scene_setup_from_cfg(): - omni.log.warn( + logger.warning( "Collision filtering can only be automatically enabled when replicate_physics=True and using GPU" " simulation. Please call scene.filter_collisions(global_prim_paths) to filter collisions across" " environments." @@ -324,7 +328,7 @@ def physics_scene_path(self) -> str: for prim in self.stage.Traverse(): if prim.HasAPI(PhysxSchema.PhysxSceneAPI): self._physics_scene_path = prim.GetPrimPath().pathString - omni.log.info(f"Physics scene prim path: {self._physics_scene_path}") + logger.info(f"Physics scene prim path: {self._physics_scene_path}") break if self._physics_scene_path is None: raise RuntimeError("No physics scene found! Please make sure one exists.") @@ -408,11 +412,11 @@ def surface_grippers(self) -> dict[str, SurfaceGripper]: return self._surface_grippers @property - def extras(self) -> dict[str, XFormPrim]: + def extras(self) -> dict[str, XformPrimView]: """A dictionary of miscellaneous simulation objects that neither inherit from assets nor sensors. - The keys are the names of the miscellaneous objects, and the values are the `XFormPrim`_ - of the corresponding prims. + The keys are the names of the miscellaneous objects, and the values are the + :class:`~isaaclab.sim.views.XformPrimView` instances of the corresponding prims. As an example, lights or other props in the scene that do not have any attributes or properties that you want to alter at runtime can be added to this dictionary. @@ -421,8 +425,6 @@ def extras(self) -> dict[str, XFormPrim]: These are not reset or updated by the scene. They are mainly other prims that are not necessarily handled by the interactive scene, but are useful to be accessed by the user. - .. _XFormPrim: https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.core.prims/docs/index.html#isaacsim.core.prims.XFormPrim - """ return self._extras @@ -768,6 +770,18 @@ def _add_entities_from_cfg(self): for filter_prim_path in asset_cfg.filter_prim_paths_expr: updated_filter_prim_paths_expr.append(filter_prim_path.format(ENV_REGEX_NS=self.env_regex_ns)) asset_cfg.filter_prim_paths_expr = updated_filter_prim_paths_expr + elif isinstance(asset_cfg, VisuoTactileSensorCfg): + if hasattr(asset_cfg, "camera_cfg") and asset_cfg.camera_cfg is not None: + asset_cfg.camera_cfg.prim_path = asset_cfg.camera_cfg.prim_path.format( + ENV_REGEX_NS=self.env_regex_ns + ) + if ( + hasattr(asset_cfg, "contact_object_prim_path_expr") + and asset_cfg.contact_object_prim_path_expr is not None + ): + asset_cfg.contact_object_prim_path_expr = asset_cfg.contact_object_prim_path_expr.format( + ENV_REGEX_NS=self.env_regex_ns + ) self._sensors[asset_name] = asset_cfg.class_type(asset_cfg) elif isinstance(asset_cfg, AssetBaseCfg): @@ -781,7 +795,7 @@ def _add_entities_from_cfg(self): ) # store xform prim view corresponding to this asset # all prims in the scene are Xform prims (i.e. have a transform component) - self._extras[asset_name] = XFormPrim(asset_cfg.prim_path, reset_xform_properties=False) + self._extras[asset_name] = XformPrimView(asset_cfg.prim_path, device=self.device, stage=self.stage) else: raise ValueError(f"Unknown asset config type for {asset_name}: {asset_cfg}") # store global collision paths diff --git a/source/isaaclab/isaaclab/scene/interactive_scene_cfg.py b/source/isaaclab/isaaclab/scene/interactive_scene_cfg.py index 2cc472ca074f..f4328324152c 100644 --- a/source/isaaclab/isaaclab/scene/interactive_scene_cfg.py +++ b/source/isaaclab/isaaclab/scene/interactive_scene_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -33,9 +33,9 @@ class InteractiveSceneCfg: from isaaclab_assets.robots.anymal import ANYMAL_C_CFG + @configclass class MySceneCfg(InteractiveSceneCfg): - # terrain - flat terrain plane terrain = TerrainImporterCfg( prim_path="/World/ground", diff --git a/source/isaaclab/isaaclab/sensors/__init__.py b/source/isaaclab/isaaclab/sensors/__init__.py index 82340483d625..f0a5719e5056 100644 --- a/source/isaaclab/isaaclab/sensors/__init__.py +++ b/source/isaaclab/isaaclab/sensors/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sensors/camera/__init__.py b/source/isaaclab/isaaclab/sensors/camera/__init__.py index 4b4497916e1a..f2318067b586 100644 --- a/source/isaaclab/isaaclab/sensors/camera/__init__.py +++ b/source/isaaclab/isaaclab/sensors/camera/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sensors/camera/camera.py b/source/isaaclab/isaaclab/sensors/camera/camera.py index 8d3fe257df62..26fd0713c715 100644 --- a/source/isaaclab/isaaclab/sensors/camera/camera.py +++ b/source/isaaclab/isaaclab/sensors/camera/camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,22 +6,22 @@ from __future__ import annotations import json -import numpy as np +import logging import re -import torch from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Literal +import numpy as np +import torch +from packaging import version + import carb -import isaacsim.core.utils.stage as stage_utils -import omni.kit.commands import omni.usd -from isaacsim.core.prims import XFormPrim -from isaacsim.core.version import get_version from pxr import Sdf, UsdGeom import isaaclab.sim as sim_utils import isaaclab.utils.sensors as sensor_utils +from isaaclab.sim.views import XformPrimView from isaaclab.utils import to_camel_case from isaaclab.utils.array import convert_to_torch from isaaclab.utils.math import ( @@ -29,6 +29,7 @@ create_rotation_matrix_from_view, quat_from_matrix, ) +from isaaclab.utils.version import get_isaac_sim_version from ..sensor_base import SensorBase from .camera_data import CameraData @@ -36,6 +37,9 @@ if TYPE_CHECKING: from .camera_cfg import CameraCfg +# import logger +logger = logging.getLogger(__name__) + class Camera(SensorBase): r"""The camera sensor for acquiring visual data. @@ -143,12 +147,11 @@ def __init__(self, cfg: CameraCfg): # Create empty variables for storing output data self._data = CameraData() - # HACK: we need to disable instancing for semantic_segmentation and instance_segmentation_fast to work - isaac_sim_version = get_version() + # HACK: We need to disable instancing for semantic_segmentation and instance_segmentation_fast to work # checks for Isaac Sim v4.5 as this issue exists there - if int(isaac_sim_version[2]) == 4 and int(isaac_sim_version[3]) == 5: + if get_isaac_sim_version() == version.parse("4.5"): if "semantic_segmentation" in self.cfg.data_types or "instance_segmentation_fast" in self.cfg.data_types: - omni.log.warn( + logger.warning( "Isaac Sim 4.5 introduced a bug in Camera and TiledCamera when outputting instance and semantic" " segmentation outputs for instanceable assets. As a workaround, the instanceable flag on assets" " will be disabled in the current workflow and may lead to longer load times and increased memory" @@ -255,7 +258,6 @@ def set_intrinsic_matrices( matrices = np.asarray(matrices, dtype=float) # iterate over env_ids for i, intrinsic_matrix in zip(env_ids, matrices): - height, width = self.image_shape params = sensor_utils.convert_camera_intrinsics_to_usd( @@ -349,7 +351,7 @@ def set_world_poses_from_view( if env_ids is None: env_ids = self._ALL_INDICES # get up axis of current stage - up_axis = stage_utils.get_stage_up_axis() + up_axis = UsdGeom.GetStageUpAxis(self.stage) # set camera poses using the view orientations = quat_from_matrix(create_rotation_matrix_from_view(eyes, targets, up_axis, device=self._device)) self._view.set_world_poses(eyes, orientations, env_ids) @@ -401,9 +403,10 @@ def _initialize_impl(self): # Initialize parent class super()._initialize_impl() - # Create a view for the sensor - self._view = XFormPrim(self.cfg.prim_path, reset_xform_properties=False) - self._view.initialize() + # Create a view for the sensor with Fabric enabled for fast pose queries, otherwise position will be stale. + self._view = XformPrimView( + self.cfg.prim_path, device=self._device, stage=self.stage, sync_usd_on_fabric_write=True + ) # Check that sizes are correct if self._view.count != self._num_envs: raise RuntimeError( @@ -421,9 +424,9 @@ def _initialize_impl(self): self._rep_registry: dict[str, list[rep.annotators.Annotator]] = {name: list() for name in self.cfg.data_types} # Convert all encapsulated prims to Camera - for cam_prim_path in self._view.prim_paths: - # Get camera prim - cam_prim = self.stage.GetPrimAtPath(cam_prim_path) + for cam_prim in self._view.prims: + # Obtain the prim path + cam_prim_path = cam_prim.GetPath().pathString # Check if prim is a camera if not cam_prim.IsA(UsdGeom.Camera): raise RuntimeError(f"Prim at path '{cam_prim_path}' is not a Camera.") diff --git a/source/isaaclab/isaaclab/sensors/camera/camera_cfg.py b/source/isaaclab/isaaclab/sensors/camera/camera_cfg.py index a123bd00b571..8fd9f307d180 100644 --- a/source/isaaclab/isaaclab/sensors/camera/camera_cfg.py +++ b/source/isaaclab/isaaclab/sensors/camera/camera_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +30,8 @@ class OffsetCfg: convention: Literal["opengl", "ros", "world"] = "ros" """The convention in which the frame offset is applied. Defaults to "ros". - - ``"opengl"`` - forward axis: ``-Z`` - up axis: ``+Y`` - Offset is applied in the OpenGL (Usd.Camera) convention. + - ``"opengl"`` - forward axis: ``-Z`` - up axis: ``+Y`` - Offset is applied in the OpenGL (Usd.Camera) + convention. - ``"ros"`` - forward axis: ``+Z`` - up axis: ``-Y`` - Offset is applied in the ROS convention. - ``"world"`` - forward axis: ``+X`` - up axis: ``+Z`` - Offset is applied in the World Frame convention. diff --git a/source/isaaclab/isaaclab/sensors/camera/camera_data.py b/source/isaaclab/isaaclab/sensors/camera/camera_data.py index dfcc780b4d11..ec3288b04e92 100644 --- a/source/isaaclab/isaaclab/sensors/camera/camera_data.py +++ b/source/isaaclab/isaaclab/sensors/camera/camera_data.py @@ -1,12 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from dataclasses import dataclass from typing import Any +import torch + from isaaclab.utils.math import convert_camera_frame_orientation_convention diff --git a/source/isaaclab/isaaclab/sensors/camera/tiled_camera.py b/source/isaaclab/isaaclab/sensors/camera/tiled_camera.py index 3e9982135c5d..4b3676158e75 100644 --- a/source/isaaclab/isaaclab/sensors/camera/tiled_camera.py +++ b/source/isaaclab/isaaclab/sensors/camera/tiled_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,17 +7,17 @@ import json import math -import numpy as np -import torch from collections.abc import Sequence from typing import TYPE_CHECKING, Any -import carb +import numpy as np +import torch import warp as wp -from isaacsim.core.prims import XFormPrim -from isaacsim.core.version import get_version + +import carb from pxr import UsdGeom +from isaaclab.sim.views import XformPrimView from isaaclab.utils.warp.kernels import reshape_tiled_image from ..sensor_base import SensorBase @@ -81,15 +81,8 @@ def __init__(self, cfg: TiledCameraCfg): Raises: RuntimeError: If no camera prim is found at the given path. - RuntimeError: If Isaac Sim version < 4.2 ValueError: If the provided data types are not supported by the camera. """ - isaac_sim_version = float(".".join(get_version()[2:4])) - if isaac_sim_version < 4.2: - raise RuntimeError( - f"TiledCamera is only available from Isaac Sim 4.2.0. Current version is {isaac_sim_version}. Please" - " update to Isaac Sim 4.2.0" - ) super().__init__(cfg) def __del__(self): @@ -158,8 +151,7 @@ def _initialize_impl(self): # Initialize parent class SensorBase._initialize_impl(self) # Create a view for the sensor - self._view = XFormPrim(self.cfg.prim_path, reset_xform_properties=False) - self._view.initialize() + self._view = XformPrimView(self.cfg.prim_path, device=self._device, stage=self.stage) # Check that sizes are correct if self._view.count != self._num_envs: raise RuntimeError( @@ -173,20 +165,19 @@ def _initialize_impl(self): self._frame = torch.zeros(self._view.count, device=self._device, dtype=torch.long) # Convert all encapsulated prims to Camera - for cam_prim_path in self._view.prim_paths: + cam_prim_paths = [] + for cam_prim in self._view.prims: # Get camera prim - cam_prim = self.stage.GetPrimAtPath(cam_prim_path) + cam_prim_path = cam_prim.GetPath().pathString # Check if prim is a camera if not cam_prim.IsA(UsdGeom.Camera): raise RuntimeError(f"Prim at path '{cam_prim_path}' is not a Camera.") # Add to list - sensor_prim = UsdGeom.Camera(cam_prim) - self._sensor_prims.append(sensor_prim) + self._sensor_prims.append(UsdGeom.Camera(cam_prim)) + cam_prim_paths.append(cam_prim_path) # Create replicator tiled render product - rp = rep.create.render_product_tiled( - cameras=self._view.prim_paths, tile_resolution=(self.cfg.width, self.cfg.height) - ) + rp = rep.create.render_product_tiled(cameras=cam_prim_paths, tile_resolution=(self.cfg.width, self.cfg.height)) self._render_product_paths = [rp.path] # Define the annotators based on requested data types @@ -271,6 +262,11 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): if data_type == "motion_vectors": tiled_data_buffer = tiled_data_buffer[:, :, :2].contiguous() + # For normals, we only require the first three channels of the tiled buffer + # Note: Not doing this breaks the alignment of the data (check: https://github.com/isaac-sim/IsaacLab/issues/4239) + if data_type == "normals": + tiled_data_buffer = tiled_data_buffer[:, :, :3].contiguous() + wp.launch( kernel=reshape_tiled_image, dim=(self._view.count, self.cfg.height, self.cfg.width), @@ -292,9 +288,9 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): # larger than the clipping range in the output. We apply an additional clipping to ensure values # are within the clipping range for all the annotators. if data_type == "distance_to_camera": - self._data.output[data_type][ - self._data.output[data_type] > self.cfg.spawn.clipping_range[1] - ] = torch.inf + self._data.output[data_type][self._data.output[data_type] > self.cfg.spawn.clipping_range[1]] = ( + torch.inf + ) # apply defined clipping behavior if ( data_type == "distance_to_camera" or data_type == "distance_to_image_plane" or data_type == "depth" diff --git a/source/isaaclab/isaaclab/sensors/camera/tiled_camera_cfg.py b/source/isaaclab/isaaclab/sensors/camera/tiled_camera_cfg.py index a14e74d6f8fb..2241a0648fd2 100644 --- a/source/isaaclab/isaaclab/sensors/camera/tiled_camera_cfg.py +++ b/source/isaaclab/isaaclab/sensors/camera/tiled_camera_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sensors/camera/utils.py b/source/isaaclab/isaaclab/sensors/camera/utils.py index 70787140bfa7..f9db81551b4f 100644 --- a/source/isaaclab/isaaclab/sensors/camera/utils.py +++ b/source/isaaclab/isaaclab/sensors/camera/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +8,10 @@ # needed to import for allowing type-hinting: torch.device | str | None from __future__ import annotations -import numpy as np -import torch from collections.abc import Sequence +import numpy as np +import torch import warp as wp import isaaclab.utils.math as math_utils @@ -170,7 +170,8 @@ def create_pointcloud_from_rgbd( The ``rgb`` attribute is used to resolve the corresponding point's color: - - If a ``np.array``/``wp.array``/``torch.tensor`` of shape (H, W, 3), then the corresponding channels encode RGB values. + - If a ``np.array``/``wp.array``/``torch.tensor`` of shape (H, W, 3), then the corresponding channels + encode the RGB values. - If a tuple, then the point cloud has a single color specified by the values (r, g, b). - If None, then default color is white, i.e. (0, 0, 0). diff --git a/source/isaaclab/isaaclab/sensors/contact_sensor/__init__.py b/source/isaaclab/isaaclab/sensors/contact_sensor/__init__.py index 07e91f88344e..94b402d41a37 100644 --- a/source/isaaclab/isaaclab/sensors/contact_sensor/__init__.py +++ b/source/isaaclab/isaaclab/sensors/contact_sensor/__init__.py @@ -1,9 +1,9 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 -"""Sub-module for rigid contact sensor based on :class:`isaacsim.core.prims.RigidContactView`.""" +"""Sub-module for rigid contact sensor.""" from .contact_sensor import ContactSensor from .contact_sensor_cfg import ContactSensorCfg diff --git a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py index aed50d390f8c..2a85a2661f6a 100644 --- a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py +++ b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +8,11 @@ from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + import carb import omni.physics.tensors.impl.api as physx from isaacsim.core.simulation_manager import SimulationManager @@ -162,8 +163,9 @@ def reset(self, env_ids: Sequence[int] | None = None): # reset contact positions if self.cfg.track_contact_points: self._data.contact_pos_w[env_ids, :] = torch.nan - # buffer used during contact position aggregation - self._contact_position_aggregate_buffer[env_ids, :] = torch.nan + # reset friction forces + if self.cfg.track_friction_forces: + self._data.friction_forces_w[env_ids, :] = 0.0 def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]: """Find bodies in the articulation based on the name keys. @@ -310,6 +312,21 @@ def _initialize_impl(self): if self.cfg.track_pose: self._data.pos_w = torch.zeros(self._num_envs, self._num_bodies, 3, device=self._device) self._data.quat_w = torch.zeros(self._num_envs, self._num_bodies, 4, device=self._device) + + # check if filter paths are valid + if self.cfg.track_contact_points or self.cfg.track_friction_forces: + if len(self.cfg.filter_prim_paths_expr) == 0: + raise ValueError( + "The 'filter_prim_paths_expr' is empty. Please specify a valid filter pattern to track" + f" {'contact points' if self.cfg.track_contact_points else 'friction forces'}." + ) + if self.cfg.max_contact_data_count_per_prim < 1: + raise ValueError( + f"The 'max_contact_data_count_per_prim' is {self.cfg.max_contact_data_count_per_prim}. " + "Please set it to a value greater than 0 to track" + f" {'contact points' if self.cfg.track_contact_points else 'friction forces'}." + ) + # -- position of contact points if self.cfg.track_contact_points: self._data.contact_pos_w = torch.full( @@ -317,10 +334,11 @@ def _initialize_impl(self): torch.nan, device=self._device, ) - # buffer used during contact position aggregation - self._contact_position_aggregate_buffer = torch.full( - (self._num_bodies * self._num_envs, self.contact_physx_view.filter_count, 3), - torch.nan, + # -- friction forces at contact points + if self.cfg.track_friction_forces: + self._data.friction_forces_w = torch.full( + (self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3), + 0.0, device=self._device, ) # -- air/contact time between contacts @@ -382,28 +400,17 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): _, buffer_contact_points, _, _, buffer_count, buffer_start_indices = ( self.contact_physx_view.get_contact_data(dt=self._sim_physics_dt) ) - # unpack the contact points: see RigidContactView.get_contact_data() documentation for details: - # https://docs.omniverse.nvidia.com/kit/docs/omni_physics/107.3/extensions/runtime/source/omni.physics.tensors/docs/api/python.html#omni.physics.tensors.impl.api.RigidContactView.get_net_contact_forces - # buffer_count: (N_envs * N_bodies, N_filters), buffer_contact_points: (N_envs * N_bodies, 3) - counts, starts = buffer_count.view(-1), buffer_start_indices.view(-1) - n_rows, total = counts.numel(), int(counts.sum()) - # default to NaN rows - agg = torch.full((n_rows, 3), float("nan"), device=self._device, dtype=buffer_contact_points.dtype) - if total > 0: - row_ids = torch.repeat_interleave(torch.arange(n_rows, device=self._device), counts) - total = row_ids.numel() - - block_starts = counts.cumsum(0) - counts - deltas = torch.arange(total, device=counts.device) - block_starts.repeat_interleave(counts) - flat_idx = starts[row_ids] + deltas - - pts = buffer_contact_points.index_select(0, flat_idx) - agg = agg.zero_().index_add_(0, row_ids, pts) / counts.clamp_min(1).unsqueeze(1) - agg[counts == 0] = float("nan") - - self._contact_position_aggregate_buffer[:] = agg.view(self._num_envs * self.num_bodies, -1, 3) - self._data.contact_pos_w[env_ids] = self._contact_position_aggregate_buffer.view( - self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3 + self._data.contact_pos_w[env_ids] = self._unpack_contact_buffer_data( + buffer_contact_points, buffer_count, buffer_start_indices + )[env_ids] + + # obtain friction forces + if self.cfg.track_friction_forces: + friction_forces, _, buffer_count, buffer_start_indices = self.contact_physx_view.get_friction_data( + dt=self._sim_physics_dt + ) + self._data.friction_forces_w[env_ids] = self._unpack_contact_buffer_data( + friction_forces, buffer_count, buffer_start_indices, avg=False, default=0.0 )[env_ids] # obtain the air time @@ -436,6 +443,58 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): is_contact, self._data.current_contact_time[env_ids] + elapsed_time.unsqueeze(-1), 0.0 ) + def _unpack_contact_buffer_data( + self, + contact_data: torch.Tensor, + buffer_count: torch.Tensor, + buffer_start_indices: torch.Tensor, + avg: bool = True, + default: float = float("nan"), + ) -> torch.Tensor: + """ + Unpacks and aggregates contact data for each (env, body, filter) group. + + This function vectorizes the following nested loop: + + for i in range(self._num_bodies * self._num_envs): + for j in range(self.contact_physx_view.filter_count): + start_index_ij = buffer_start_indices[i, j] + count_ij = buffer_count[i, j] + self._contact_position_aggregate_buffer[i, j, :] = torch.mean( + contact_data[start_index_ij : (start_index_ij + count_ij), :], dim=0 + ) + + For more details, see the `RigidContactView.get_contact_data() documentation `_. + + Args: + contact_data: Flat tensor of contact data, shape (N_envs * N_bodies, 3). + buffer_count: Number of contact points per (env, body, filter), shape (N_envs * N_bodies, N_filters). + buffer_start_indices: Start indices for each (env, body, filter), shape (N_envs * N_bodies, N_filters). + avg: If True, average the contact data for each group; if False, sum the data. Defaults to True. + default: Default value to use for groups with zero contacts. Defaults to NaN. + + Returns: + Aggregated contact data, shape (N_envs, N_bodies, N_filters, 3). + """ + counts, starts = buffer_count.view(-1), buffer_start_indices.view(-1) + n_rows, total = counts.numel(), int(counts.sum()) + agg = torch.full((n_rows, 3), default, device=self._device, dtype=contact_data.dtype) + if total > 0: + row_ids = torch.repeat_interleave(torch.arange(n_rows, device=self._device), counts) + + block_starts = counts.cumsum(0) - counts + deltas = torch.arange(row_ids.numel(), device=counts.device) - block_starts.repeat_interleave(counts) + flat_idx = starts[row_ids] + deltas + + pts = contact_data.index_select(0, flat_idx) + agg = agg.zero_().index_add_(0, row_ids, pts) + agg = agg / counts.clamp_min(1).unsqueeze(-1) if avg else agg + agg[counts == 0] = default + + return agg.view(self._num_envs * self.num_bodies, -1, 3).view( + self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3 + ) + def _set_debug_vis_impl(self, debug_vis: bool): # set visibility of markers # note: parent only deals with callbacks. not their visibility diff --git a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py index c51b09473bb7..c811a7ca63d1 100644 --- a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py +++ b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -23,6 +23,9 @@ class ContactSensorCfg(SensorBaseCfg): track_contact_points: bool = False """Whether to track the contact point locations. Defaults to False.""" + track_friction_forces: bool = False + """Whether to track the friction forces at the contact points. Defaults to False.""" + max_contact_data_count_per_prim: int = 4 """The maximum number of contacts across all batches of the sensor to keep track of. Default is 4. diff --git a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_data.py b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_data.py index 5d08f6058ce8..fd6c15ebe960 100644 --- a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_data.py +++ b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_data.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,9 +6,10 @@ # needed to import for allowing type-hinting: torch.Tensor | None from __future__ import annotations -import torch from dataclasses import dataclass +import torch + @dataclass class ContactSensorData: @@ -35,9 +36,29 @@ class ContactSensorData: Note: * If the :attr:`ContactSensorCfg.track_contact_points` is False, then this quantity is None. - * If the :attr:`ContactSensorCfg.filter_prim_paths_expr` is empty, then this quantity is an empty tensor. - * If the :attr:`ContactSensorCfg.max_contact_data_per_prim` is not specified or less than 1, then this quantity - will not be calculated. + * If the :attr:`ContactSensorCfg.track_contact_points` is True, a ValueError will be raised if: + + * If the :attr:`ContactSensorCfg.filter_prim_paths_expr` is empty. + * If the :attr:`ContactSensorCfg.max_contact_data_per_prim` is not specified or less than 1. + will not be calculated. + + """ + + friction_forces_w: torch.Tensor | None = None + """Sum of the friction forces between sensor body and filter prim in world frame. + + Shape is (N, B, M, 3), where N is the number of sensors, B is number of bodies in each sensor + and M is the number of filtered bodies. + + Collision pairs not in contact will result in NaN. + + Note: + + * If the :attr:`ContactSensorCfg.track_friction_forces` is False, then this quantity is None. + * If the :attr:`ContactSensorCfg.track_friction_forces` is True, a ValueError will be raised if: + + * The :attr:`ContactSensorCfg.filter_prim_paths_expr` is empty. + * The :attr:`ContactSensorCfg.max_contact_data_per_prim` is not specified or less than 1. """ diff --git a/source/isaaclab/isaaclab/sensors/frame_transformer/__init__.py b/source/isaaclab/isaaclab/sensors/frame_transformer/__init__.py index 5844fa629d17..d5db853e8cc2 100644 --- a/source/isaaclab/isaaclab/sensors/frame_transformer/__init__.py +++ b/source/isaaclab/isaaclab/sensors/frame_transformer/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer.py b/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer.py index ae7811d31ec5..ed83392b3aa7 100644 --- a/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer.py +++ b/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer.py @@ -1,16 +1,17 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +import logging import re -import torch from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log +import torch + from isaacsim.core.simulation_manager import SimulationManager from pxr import UsdPhysics @@ -32,6 +33,9 @@ if TYPE_CHECKING: from .frame_transformer_cfg import FrameTransformerCfg +# import logger +logger = logging.getLogger(__name__) + class FrameTransformer(SensorBase): """A sensor for reporting frame transforms. @@ -149,16 +153,17 @@ def _initialize_impl(self): self._apply_source_frame_offset = True # Handle source frame offsets if is_identity_pose(source_frame_offset_pos, source_frame_offset_quat): - omni.log.verbose(f"No offset application needed for source frame as it is identity: {self.cfg.prim_path}") + logger.debug(f"No offset application needed for source frame as it is identity: {self.cfg.prim_path}") self._apply_source_frame_offset = False else: - omni.log.verbose(f"Applying offset to source frame as it is not identity: {self.cfg.prim_path}") + logger.debug(f"Applying offset to source frame as it is not identity: {self.cfg.prim_path}") # Store offsets as tensors (duplicating each env's offsets for ease of multiplication later) self._source_frame_offset_pos = source_frame_offset_pos.unsqueeze(0).repeat(self._num_envs, 1) self._source_frame_offset_quat = source_frame_offset_quat.unsqueeze(0).repeat(self._num_envs, 1) - # Keep track of mapping from the rigid body name to the desired frames and prim path, as there may be multiple frames - # based upon the same body name and we don't want to create unnecessary views + # Keep track of mapping from the rigid body name to the desired frames and prim path, + # as there may be multiple frames based upon the same body name and we don't want to + # create unnecessary views. body_names_to_frames: dict[str, dict[str, set[str] | str]] = {} # The offsets associated with each target frame target_offsets: dict[str, dict[str, torch.Tensor]] = {} @@ -198,10 +203,10 @@ def _initialize_impl(self): " rigid body. The class only supports transformations between rigid bodies." ) - # Get the name of the body - body_name = matching_prim_path.rsplit("/", 1)[-1] - # Use body name if frame isn't specified by user - frame_name = frame if frame is not None else body_name + # Get the name of the body: use relative prim path for unique identification + body_name = self._get_relative_body_path(matching_prim_path) + # Use leaf name of prim path if frame name isn't specified by user + frame_name = frame if frame is not None else matching_prim_path.rsplit("/", 1)[-1] # Keep track of which frames are associated with which bodies if body_name in body_names_to_frames: @@ -229,12 +234,12 @@ def _initialize_impl(self): target_offsets[frame_name] = {"pos": offset_pos, "quat": offset_quat} if not self._apply_target_frame_offset: - omni.log.info( + logger.info( f"No offsets application needed from '{self.cfg.prim_path}' to target frames as all" f" are identity: {frames[1:]}" ) else: - omni.log.info( + logger.info( f"Offsets application needed from '{self.cfg.prim_path}' to the following target frames:" f" {non_identity_offset_frames}" ) @@ -269,8 +274,9 @@ def extract_env_num_and_prim_path(item: str) -> tuple[int, str]: match = re.search(r"env_(\d+)(.*)", item) return (int(match.group(1)), match.group(2)) - # Find the indices that would reorganize output to be per environment. We want `env_1/blah` to come before `env_11/blah` - # and env_1/Robot/base to come before env_1/Robot/foot so we need to use custom key function + # Find the indices that would reorganize output to be per environment. + # We want `env_1/blah` to come before `env_11/blah` and env_1/Robot/base + # to come before env_1/Robot/foot so we need to use custom key function self._per_env_indices = [ index for index, _ in sorted( @@ -284,15 +290,16 @@ def extract_env_num_and_prim_path(item: str) -> tuple[int, str]: ] else: - # If no environment is present, then the order of the body names is the same as the order of the prim paths sorted alphabetically + # If no environment is present, then the order of the body names is the same as the order of the + # prim paths sorted alphabetically self._per_env_indices = [index for index, _ in sorted(enumerate(all_prim_paths), key=lambda x: x[1])] sorted_prim_paths = [all_prim_paths[index] for index in self._per_env_indices] - # -- target frames - self._target_frame_body_names = [prim_path.split("/")[-1] for prim_path in sorted_prim_paths] + # -- target frames: use relative prim path for unique identification + self._target_frame_body_names = [self._get_relative_body_path(prim_path) for prim_path in sorted_prim_paths] - # -- source frame - self._source_frame_body_name = self.cfg.prim_path.split("/")[-1] + # -- source frame: use relative prim path for unique identification + self._source_frame_body_name = self._get_relative_body_path(self.cfg.prim_path) source_frame_index = self._target_frame_body_names.index(self._source_frame_body_name) # Only remove source frame from tracked bodies if it is not also a target frame @@ -303,7 +310,8 @@ def extract_env_num_and_prim_path(item: str) -> tuple[int, str]: all_ids = torch.arange(self._num_envs * len(tracked_body_names)) self._source_frame_body_ids = torch.arange(self._num_envs) * len(tracked_body_names) + source_frame_index - # If source frame is also a target frame, then the target frame body ids are the same as the source frame body ids + # If source frame is also a target frame, then the target frame body ids are the same as + # the source frame body ids if self._source_is_also_target_frame: self._target_frame_body_ids = all_ids else: @@ -483,17 +491,20 @@ def _invalidate_initialize_callback(self, event): def _get_connecting_lines( self, start_pos: torch.Tensor, end_pos: torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - """ - Given start and end points, compute the positions (mid-point), orientations, and lengths of the connecting lines. + """Draws connecting lines between frames. + + Given start and end points, this function computes the positions (mid-point), orientations, + and lengths of the connecting lines. Args: start_pos: The start positions of the connecting lines. Shape is (N, 3). end_pos: The end positions of the connecting lines. Shape is (N, 3). Returns: - positions: The position of each connecting line. Shape is (N, 3). - orientations: The orientation of each connecting line in quaternion. Shape is (N, 4). - lengths: The length of each connecting line. Shape is (N,). + A tuple containing: + - The positions of each connecting line. Shape is (N, 3). + - The orientations of each connecting line in quaternion. Shape is (N, 4). + - The lengths of each connecting line. Shape is (N,). """ direction = end_pos - start_pos lengths = torch.norm(direction, dim=-1) @@ -524,3 +535,26 @@ def _get_connecting_lines( orientations = quat_from_angle_axis(angle, rotation_axis) return positions, orientations, lengths + + @staticmethod + def _get_relative_body_path(prim_path: str) -> str: + """Extract a normalized body path from a prim path. + + Removes the environment instance segment `/envs/env_/` to normalize paths + across multiple environments, while preserving the `/envs/` prefix to + distinguish environment-scoped paths from non-environment paths. + + Examples: + - '/World/envs/env_0/Robot/torso' -> '/World/envs/Robot/torso' + - '/World/envs/env_123/Robot/left_hand' -> '/World/envs/Robot/left_hand' + - '/World/Robot' -> '/World/Robot' + - '/World/Robot_2/left_hand' -> '/World/Robot_2/left_hand' + + Args: + prim_path: The full prim path. + + Returns: + The prim path with `/envs/env_/` removed, preserving `/envs/`. + """ + pattern = re.compile(r"/envs/env_[^/]+/") + return pattern.sub("/envs/", prim_path) diff --git a/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_cfg.py b/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_cfg.py index 2c24b7585cee..ca9b528aa1d9 100644 --- a/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_cfg.py +++ b/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -33,11 +33,14 @@ class FrameCfg: prim_path: str = MISSING """The prim path corresponding to a rigid body. - This can be a regex pattern to match multiple prims. For example, "/Robot/.*" will match all prims under "/Robot". + This can be a regex pattern to match multiple prims. For example, "/Robot/.*" + will match all prims under "/Robot". - This means that if the source :attr:`FrameTransformerCfg.prim_path` is "/Robot/base", and the target :attr:`FrameTransformerCfg.FrameCfg.prim_path` is "/Robot/.*", - then the frame transformer will track the poses of all the prims under "/Robot", - including "/Robot/base" (even though this will result in an identity pose w.r.t. the source frame). + This means that if the source :attr:`FrameTransformerCfg.prim_path` is "/Robot/base", + and the target :attr:`FrameTransformerCfg.FrameCfg.prim_path` is "/Robot/.*", then + the frame transformer will track the poses of all the prims under "/Robot", + including "/Robot/base" (even though this will result in an identity pose w.r.t. + the source frame). """ name: str | None = None diff --git a/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_data.py b/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_data.py index d66ea481f069..942ddbd5144b 100644 --- a/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_data.py +++ b/source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer_data.py @@ -1,11 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from dataclasses import dataclass +import torch + @dataclass class FrameTransformerData: diff --git a/source/isaaclab/isaaclab/sensors/imu/__init__.py b/source/isaaclab/isaaclab/sensors/imu/__init__.py index 31aeabf37eb9..7501e41cf49d 100644 --- a/source/isaaclab/isaaclab/sensors/imu/__init__.py +++ b/source/isaaclab/isaaclab/sensors/imu/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sensors/imu/imu.py b/source/isaaclab/isaaclab/sensors/imu/imu.py index 1c700eeedb21..e092b39502ee 100644 --- a/source/isaaclab/isaaclab/sensors/imu/imu.py +++ b/source/isaaclab/isaaclab/sensors/imu/imu.py @@ -1,17 +1,17 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING -import isaacsim.core.utils.stage as stage_utils +import torch + from isaacsim.core.simulation_manager import SimulationManager -from pxr import UsdPhysics +from pxr import UsdGeom, UsdPhysics import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils @@ -27,10 +27,13 @@ class Imu(SensorBase): """The Inertia Measurement Unit (IMU) sensor. - The sensor can be attached to any :class:`RigidObject` or :class:`Articulation` in the scene. The sensor provides complete state information. - The sensor is primarily used to provide the linear acceleration and angular velocity of the object in the body frame. The sensor also provides - the position and orientation of the object in the world frame and the angular acceleration and linear velocity in the body frame. The extra - data outputs are useful for simulating with or comparing against "perfect" state estimation. + The sensor can be attached to any prim path with a rigid ancestor in its tree and produces body-frame + linear acceleration and angular velocity, along with world-frame pose and body-frame linear and angular + accelerations/velocities. + + If the provided path is not a rigid body, the closest rigid-body ancestor is used for simulation queries. + The fixed transform from that ancestor to the target prim is computed once during initialization and + composed with the configured sensor offset. .. note:: @@ -40,10 +43,13 @@ class Imu(SensorBase): .. note:: - It is suggested to use the OffsetCfg to define an IMU frame relative to a rigid body prim defined at the root of - a :class:`RigidObject` or a prim that is defined by a non-fixed joint in an :class:`Articulation` (except for the - root of a fixed based articulation). The use frames with fixed joints and small mass/inertia to emulate a transform - relative to a body frame can result in lower performance and accuracy. + The user can configure the sensor offset in the configuration file. The offset is applied relative to the + rigid source prim. If the target prim is not a rigid body, the offset is composed with the fixed transform + from the rigid ancestor to the target prim. The offset is applied in the body frame of the rigid source prim. + The offset is defined as a position vector and a quaternion rotation, which + are applied in the order: position, then rotation. The position is applied as a translation + in the body frame of the rigid source prim, and the rotation is applied as a rotation + in the body frame of the rigid source prim. """ @@ -61,6 +67,9 @@ def __init__(self, cfg: ImuCfg): # Create empty variables for storing output data self._data = ImuData() + # Internal: expression used to build the rigid body view (may be different from cfg.prim_path) + self._rigid_parent_expr: str | None = None + def __str__(self) -> str: """Returns: A string containing information about the instance.""" return ( @@ -105,6 +114,8 @@ def reset(self, env_ids: Sequence[int] | None = None): self._data.ang_vel_b[env_ids] = 0.0 self._data.lin_acc_b[env_ids] = 0.0 self._data.ang_acc_b[env_ids] = 0.0 + self._prev_lin_vel_w[env_ids] = 0.0 + self._prev_ang_vel_w[env_ids] = 0.0 def update(self, dt: float, force_recompute: bool = False): # save timestamp @@ -119,11 +130,9 @@ def update(self, dt: float, force_recompute: bool = False): def _initialize_impl(self): """Initializes the sensor handles and internal buffers. - This function creates handles and registers the provided data types with the replicator registry to - be able to access the data from the sensor. It also initializes the internal buffers to store the data. - - Raises: - RuntimeError: If the imu prim is not a RigidBodyPrim + - If the target prim path is a rigid body, build the view directly on it. + - Otherwise find the closest rigid-body ancestor, cache the fixed transform from that ancestor + to the target prim, and build the view on the ancestor expression. """ # Initialize parent class super()._initialize_impl() @@ -133,11 +142,26 @@ def _initialize_impl(self): prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) if prim is None: raise RuntimeError(f"Failed to find a prim at path expression: {self.cfg.prim_path}") - # check if it is a RigidBody Prim - if prim.HasAPI(UsdPhysics.RigidBodyAPI): - self._view = self._physics_sim_view.create_rigid_body_view(self.cfg.prim_path.replace(".*", "*")) + + # Find the first matching ancestor prim that implements rigid body API + ancestor_prim = sim_utils.get_first_matching_ancestor_prim( + prim.GetPath(), predicate=lambda _prim: _prim.HasAPI(UsdPhysics.RigidBodyAPI) + ) + if ancestor_prim is None: + raise RuntimeError(f"Failed to find a rigid body ancestor prim at path expression: {self.cfg.prim_path}") + # Convert ancestor prim path to expression + if ancestor_prim == prim: + self._rigid_parent_expr = self.cfg.prim_path + fixed_pos_b, fixed_quat_b = None, None else: - raise RuntimeError(f"Failed to find a RigidBodyAPI for the prim paths: {self.cfg.prim_path}") + # Convert ancestor prim path to expression + relative_path = prim.GetPath().MakeRelativePath(ancestor_prim.GetPath()).pathString + self._rigid_parent_expr = self.cfg.prim_path.replace(relative_path, "") + # Resolve the relative pose between the target prim and the ancestor prim + fixed_pos_b, fixed_quat_b = sim_utils.resolve_prim_pose(prim, ancestor_prim) + + # Create the rigid body view on the ancestor + self._view = self._physics_sim_view.create_rigid_body_view(self._rigid_parent_expr.replace(".*", "*")) # Get world gravity gravity = self._physics_sim_view.get_gravity() @@ -148,35 +172,53 @@ def _initialize_impl(self): # Create internal buffers self._initialize_buffers_impl() + # Compose the configured offset with the fixed ancestor->target transform (done once) + # new_offset = fixed * cfg.offset + # where composition is: p = p_fixed + R_fixed * p_cfg, q = q_fixed * q_cfg + if fixed_pos_b is not None and fixed_quat_b is not None: + # Broadcast fixed transform across instances + fixed_p = torch.tensor(fixed_pos_b, device=self._device).repeat(self._view.count, 1) + fixed_q = torch.tensor(fixed_quat_b, device=self._device).repeat(self._view.count, 1) + + cfg_p = self._offset_pos_b.clone() + cfg_q = self._offset_quat_b.clone() + + composed_p = fixed_p + math_utils.quat_apply(fixed_q, cfg_p) + composed_q = math_utils.quat_mul(fixed_q, cfg_q) + + self._offset_pos_b = composed_p + self._offset_quat_b = composed_q + def _update_buffers_impl(self, env_ids: Sequence[int]): """Fills the buffers of the sensor data.""" # default to all sensors if len(env_ids) == self._num_envs: env_ids = slice(None) - # obtain the poses of the sensors + # world pose of the rigid source (ancestor) from the PhysX view pos_w, quat_w = self._view.get_transforms()[env_ids].split([3, 4], dim=-1) quat_w = quat_w.roll(1, dims=-1) - # store the poses + # sensor pose in world: apply composed offset self._data.pos_w[env_ids] = pos_w + math_utils.quat_apply(quat_w, self._offset_pos_b[env_ids]) self._data.quat_w[env_ids] = math_utils.quat_mul(quat_w, self._offset_quat_b[env_ids]) - # get the offset from COM to link origin + # COM of rigid source (body frame) com_pos_b = self._view.get_coms().to(self.device).split([3, 4], dim=-1)[0] - # obtain the velocities of the link COM + # Velocities at rigid source COM lin_vel_w, ang_vel_w = self._view.get_velocities()[env_ids].split([3, 3], dim=-1) - # if an offset is present or the COM does not agree with the link origin, the linear velocity has to be - # transformed taking the angular velocity into account + + # If sensor offset or COM != link origin, account for angular velocity contribution lin_vel_w += torch.linalg.cross( ang_vel_w, math_utils.quat_apply(quat_w, self._offset_pos_b[env_ids] - com_pos_b[env_ids]), dim=-1 ) - # numerical derivative + # numerical derivative (world frame) lin_acc_w = (lin_vel_w - self._prev_lin_vel_w[env_ids]) / self._dt + self._gravity_bias_w[env_ids] ang_acc_w = (ang_vel_w - self._prev_ang_vel_w[env_ids]) / self._dt - # stack data in world frame and batch rotate + + # batch rotate world->body using current sensor orientation dynamics_data = torch.stack((lin_vel_w, ang_vel_w, lin_acc_w, ang_acc_w, self.GRAVITY_VEC_W[env_ids]), dim=0) dynamics_data_rot = math_utils.quat_apply_inverse(self._data.quat_w[env_ids].repeat(5, 1), dynamics_data).chunk( 5, dim=0 @@ -207,7 +249,8 @@ def _initialize_buffers_impl(self): self._prev_lin_vel_w = torch.zeros_like(self._data.pos_w) self._prev_ang_vel_w = torch.zeros_like(self._data.pos_w) - # store sensor offset transformation + # store sensor offset (applied relative to rigid source). + # This may be composed later with a fixed ancestor->target transform. self._offset_pos_b = torch.tensor(list(self.cfg.offset.pos), device=self._device).repeat(self._view.count, 1) self._offset_quat_b = torch.tensor(list(self.cfg.offset.rot), device=self._device).repeat(self._view.count, 1) # set gravity bias @@ -241,7 +284,7 @@ def _debug_vis_callback(self, event): default_scale = self.acceleration_visualizer.cfg.markers["arrow"].scale arrow_scale = torch.tensor(default_scale, device=self.device).repeat(self._data.lin_acc_b.shape[0], 1) # get up axis of current stage - up_axis = stage_utils.get_stage_up_axis() + up_axis = UsdGeom.GetStageUpAxis(self.stage) # arrow-direction quat_opengl = math_utils.quat_from_matrix( math_utils.create_rotation_matrix_from_view( diff --git a/source/isaaclab/isaaclab/sensors/imu/imu_cfg.py b/source/isaaclab/isaaclab/sensors/imu/imu_cfg.py index edbb93ff385f..06aeca5fa95b 100644 --- a/source/isaaclab/isaaclab/sensors/imu/imu_cfg.py +++ b/source/isaaclab/isaaclab/sensors/imu/imu_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sensors/imu/imu_data.py b/source/isaaclab/isaaclab/sensors/imu/imu_data.py index ae8efc47831b..dd06e09a8b79 100644 --- a/source/isaaclab/isaaclab/sensors/imu/imu_data.py +++ b/source/isaaclab/isaaclab/sensors/imu/imu_data.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from dataclasses import dataclass +import torch + @dataclass class ImuData: diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/__init__.py b/source/isaaclab/isaaclab/sensors/ray_caster/__init__.py index a4fe1bce5199..06f479ed2ee8 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/__init__.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/__init__.py @@ -1,11 +1,27 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 -"""Sub-module for Warp-based ray-cast sensor.""" +"""Sub-module for Warp-based ray-cast sensor. + +The sub-module contains two implementations of the ray-cast sensor: + +- :class:`isaaclab.sensors.ray_caster.RayCaster`: A basic ray-cast sensor that can be used to ray-cast against a single mesh. +- :class:`isaaclab.sensors.ray_caster.MultiMeshRayCaster`: A multi-mesh ray-cast sensor that can be used to ray-cast against + multiple meshes. For these meshes, it tracks their transformations and updates the warp meshes accordingly. + +Corresponding camera implementations are also provided for each of the sensor implementations. Internally, they perform +the same ray-casting operations as the sensor implementations, but return the results as images. +""" from . import patterns +from .multi_mesh_ray_caster import MultiMeshRayCaster +from .multi_mesh_ray_caster_camera import MultiMeshRayCasterCamera +from .multi_mesh_ray_caster_camera_cfg import MultiMeshRayCasterCameraCfg +from .multi_mesh_ray_caster_camera_data import MultiMeshRayCasterCameraData +from .multi_mesh_ray_caster_cfg import MultiMeshRayCasterCfg +from .multi_mesh_ray_caster_data import MultiMeshRayCasterData from .ray_caster import RayCaster from .ray_caster_camera import RayCasterCamera from .ray_caster_camera_cfg import RayCasterCameraCfg diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster.py b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster.py new file mode 100644 index 000000000000..39be0d7ca0d8 --- /dev/null +++ b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster.py @@ -0,0 +1,427 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import logging +import re +from collections.abc import Sequence +from typing import TYPE_CHECKING, ClassVar + +import numpy as np +import torch +import trimesh +import warp as wp + +import omni.physics.tensors.impl.api as physx + +import isaaclab.sim as sim_utils +from isaaclab.sim.views import XformPrimView +from isaaclab.utils.math import matrix_from_quat, quat_mul +from isaaclab.utils.mesh import PRIMITIVE_MESH_TYPES, create_trimesh_from_geom_mesh, create_trimesh_from_geom_shape +from isaaclab.utils.warp import convert_to_warp_mesh, raycast_dynamic_meshes + +from .multi_mesh_ray_caster_data import MultiMeshRayCasterData +from .ray_cast_utils import obtain_world_pose_from_view +from .ray_caster import RayCaster + +if TYPE_CHECKING: + from .multi_mesh_ray_caster_cfg import MultiMeshRayCasterCfg + +# import logger +logger = logging.getLogger(__name__) + + +class MultiMeshRayCaster(RayCaster): + """A multi-mesh ray-casting sensor. + + The ray-caster uses a set of rays to detect collisions with meshes in the scene. The rays are + defined in the sensor's local coordinate frame. The sensor can be configured to ray-cast against + a set of meshes with a given ray pattern. + + The meshes are parsed from the list of primitive paths provided in the configuration. These are then + converted to warp meshes and stored in the :attr:`meshes` list. The ray-caster then ray-casts against + these warp meshes using the ray pattern provided in the configuration. + + Compared to the default RayCaster, the MultiMeshRayCaster provides additional functionality and flexibility as + an extension of the default RayCaster with the following enhancements: + + - Raycasting against multiple target types : Supports primitive shapes (spheres, cubes, etc.) as well as arbitrary + meshes. + - Dynamic mesh tracking : Keeps track of specified meshes, enabling raycasting against moving parts + (e.g., robot links, articulated bodies, or dynamic obstacles). + - Memory-efficient caching : Avoids redundant memory usage by reusing mesh data across environments. + + Example usage to raycast against the visual meshes of a robot (e.g. ANYmal): + + .. code-block:: python + + ray_caster_cfg = MultiMeshRayCasterCfg( + prim_path="{ENV_REGEX_NS}/Robot", + mesh_prim_paths=[ + "/World/Ground", + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LF_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RF_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LH_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RH_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/base/visuals"), + ], + ray_alignment="world", + pattern_cfg=patterns.GridPatternCfg(resolution=0.02, size=(2.5, 2.5), direction=(0, 0, -1)), + ) + + """ + + cfg: MultiMeshRayCasterCfg + """The configuration parameters.""" + + mesh_offsets: dict[str, tuple[torch.Tensor, torch.Tensor]] = {} + + mesh_views: ClassVar[dict[str, XformPrimView | physx.ArticulationView | physx.RigidBodyView]] = {} + """A dictionary to store mesh views for raycasting, shared across all instances. + + The keys correspond to the prim path for the mesh views, and values are the corresponding view objects. + """ + + def __init__(self, cfg: MultiMeshRayCasterCfg): + """Initializes the ray-caster object. + + Args: + cfg: The configuration parameters. + """ + # Initialize base class + super().__init__(cfg) + + # Create empty variables for storing output data + self._num_meshes_per_env: dict[str, int] = {} + """Keeps track of the number of meshes per env for each ray_cast target. + Since we allow regex indexing (e.g. env_*/object_*) they can differ + """ + + self._raycast_targets_cfg: list[MultiMeshRayCasterCfg.RaycastTargetCfg] = [] + for target in self.cfg.mesh_prim_paths: + # Legacy support for string targets. Treat them as global targets. + if isinstance(target, str): + self._raycast_targets_cfg.append(cfg.RaycastTargetCfg(prim_expr=target, track_mesh_transforms=False)) + else: + self._raycast_targets_cfg.append(target) + + # Resolve regex namespace if set + for cfg in self._raycast_targets_cfg: + cfg.prim_expr = cfg.prim_expr.format(ENV_REGEX_NS="/World/envs/env_.*") + + # overwrite the data class + self._data = MultiMeshRayCasterData() + + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + + return ( + f"Ray-caster @ '{self.cfg.prim_path}': \n" + f"\tview type : {self._view.__class__}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tnumber of meshes : {self._num_envs} x {sum(self._num_meshes_per_env.values())} \n" + f"\tnumber of sensors : {self._view.count}\n" + f"\tnumber of rays/sensor: {self.num_rays}\n" + f"\ttotal number of rays : {self.num_rays * self._view.count}" + ) + + """ + Properties + """ + + @property + def data(self) -> MultiMeshRayCasterData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + """ + Implementation. + """ + + def _initialize_warp_meshes(self): + """Parse mesh prim expressions, build (or reuse) Warp meshes, and cache per-env mesh IDs. + + High-level steps (per target expression): + + 1. Resolve matching prims by regex/path expression. + 2. Collect supported mesh child prims; merge into a single mesh if configured. + 3. Deduplicate identical vertex buffers (exact match) to avoid uploading duplicates to Warp. + 4. Partition mesh IDs per environment or mark as globally shared. + 5. Optionally create physics views (articulation / rigid body / fallback XForm) and cache local offsets. + + Exceptions: + Raises a RuntimeError if: + + - No prims match the provided expression. + - No supported mesh prims are found under a matched prim. + - Multiple mesh prims are found but merging is disabled. + + """ + multi_mesh_ids: dict[str, list[list[int]]] = {} + for target_cfg in self._raycast_targets_cfg: + # target prim path to ray cast against + target_prim_path = target_cfg.prim_expr + # # check if mesh already casted into warp mesh and skip if so. + if target_prim_path in multi_mesh_ids: + logger.warning( + f"Mesh at target prim path '{target_prim_path}' already exists in the mesh cache. Duplicate entries" + " in `mesh_prim_paths`? This mesh will be skipped." + ) + continue + + # find all matching prim paths to provided expression of the target + target_prims = sim_utils.find_matching_prims(target_prim_path) + if len(target_prims) == 0: + raise RuntimeError(f"Failed to find a prim at path expression: {target_prim_path}") + + # If only one prim is found, treat it as a global prim. + # Either it's a single global object (e.g. ground) or we are only using one env. + is_global_prim = len(target_prims) == 1 + + loaded_vertices: list[np.ndarray | None] = [] + wp_mesh_ids = [] + + for target_prim in target_prims: + # Reuse previously parsed shared mesh instance if possible. + if target_cfg.is_shared and len(wp_mesh_ids) > 0: + # Verify if this mesh has already been registered in an earlier environment. + # Note, this check may fail, if the prim path is not following the env_.* pattern + # Which (worst case) leads to parsing the mesh and skipping registering it at a later stage + curr_prim_base_path = re.sub(r"env_\d+", "env_0", str(target_prim.GetPath())) # + if curr_prim_base_path in MultiMeshRayCaster.meshes: + MultiMeshRayCaster.meshes[str(target_prim.GetPath())] = MultiMeshRayCaster.meshes[ + curr_prim_base_path + ] + # Reuse mesh imported by another ray-cast sensor (global cache). + if str(target_prim.GetPath()) in MultiMeshRayCaster.meshes: + wp_mesh_ids.append(MultiMeshRayCaster.meshes[str(target_prim.GetPath())].id) + loaded_vertices.append(None) + continue + + mesh_prims = sim_utils.get_all_matching_child_prims( + target_prim.GetPath(), lambda prim: prim.GetTypeName() in PRIMITIVE_MESH_TYPES + ["Mesh"] + ) + if len(mesh_prims) == 0: + warn_msg = ( + f"No mesh prims found at path: {target_prim.GetPath()} with supported types:" + f" {PRIMITIVE_MESH_TYPES + ['Mesh']}" + " Skipping this target." + ) + for prim in sim_utils.get_all_matching_child_prims(target_prim.GetPath(), lambda prim: True): + warn_msg += f"\n - Available prim '{prim.GetPath()}' of type '{prim.GetTypeName()}'" + logger.warning(warn_msg) + continue + + trimesh_meshes = [] + + for mesh_prim in mesh_prims: + # check if valid + if mesh_prim is None or not mesh_prim.IsValid(): + raise RuntimeError(f"Invalid mesh prim path: {target_prim}") + + if mesh_prim.GetTypeName() == "Mesh": + mesh = create_trimesh_from_geom_mesh(mesh_prim) + else: + mesh = create_trimesh_from_geom_shape(mesh_prim) + scale = sim_utils.resolve_prim_scale(mesh_prim) + mesh.apply_scale(scale) + + relative_pos, relative_quat = sim_utils.resolve_prim_pose(mesh_prim, target_prim) + relative_pos = torch.tensor(relative_pos, dtype=torch.float32) + relative_quat = torch.tensor(relative_quat, dtype=torch.float32) + + rotation = matrix_from_quat(relative_quat) + transform = np.eye(4) + transform[:3, :3] = rotation.numpy() + transform[:3, 3] = relative_pos.numpy() + mesh.apply_transform(transform) + + # add to list of parsed meshes + trimesh_meshes.append(mesh) + + if len(trimesh_meshes) == 1: + trimesh_mesh = trimesh_meshes[0] + elif target_cfg.merge_prim_meshes: + # combine all trimesh meshes into a single mesh + trimesh_mesh = trimesh.util.concatenate(trimesh_meshes) + else: + raise RuntimeError( + f"Multiple mesh prims found at path: {target_prim.GetPath()} but merging is disabled. Please" + " enable `merge_prim_meshes` in the configuration or specify each mesh separately." + ) + + # check if the mesh is already registered, if so only reference the mesh + registered_idx = _registered_points_idx(trimesh_mesh.vertices, loaded_vertices) + if registered_idx != -1 and self.cfg.reference_meshes: + logger.info("Found a duplicate mesh, only reference the mesh.") + # Found a duplicate mesh, only reference the mesh. + loaded_vertices.append(None) + wp_mesh_ids.append(wp_mesh_ids[registered_idx]) + else: + loaded_vertices.append(trimesh_mesh.vertices) + wp_mesh = convert_to_warp_mesh(trimesh_mesh.vertices, trimesh_mesh.faces, device=self.device) + MultiMeshRayCaster.meshes[str(target_prim.GetPath())] = wp_mesh + wp_mesh_ids.append(wp_mesh.id) + + # print info + if registered_idx != -1: + logger.info(f"Found duplicate mesh for mesh prims under path '{target_prim.GetPath()}'.") + else: + logger.info( + f"Read '{len(mesh_prims)}' mesh prims under path '{target_prim.GetPath()}' with" + f" {len(trimesh_mesh.vertices)} vertices and {len(trimesh_mesh.faces)} faces." + ) + + if is_global_prim: + # reference the mesh for each environment to ray cast against + multi_mesh_ids[target_prim_path] = [wp_mesh_ids] * self._num_envs + self._num_meshes_per_env[target_prim_path] = len(wp_mesh_ids) + else: + # split up the meshes for each environment. Little bit ugly, since + # the current order is interleaved (env1_obj1, env1_obj2, env2_obj1, env2_obj2, ...) + multi_mesh_ids[target_prim_path] = [] + mesh_idx = 0 + n_meshes_per_env = len(wp_mesh_ids) // self._num_envs + self._num_meshes_per_env[target_prim_path] = n_meshes_per_env + for _ in range(self._num_envs): + multi_mesh_ids[target_prim_path].append(wp_mesh_ids[mesh_idx : mesh_idx + n_meshes_per_env]) + mesh_idx += n_meshes_per_env + + if target_cfg.track_mesh_transforms: + MultiMeshRayCaster.mesh_views[target_prim_path], MultiMeshRayCaster.mesh_offsets[target_prim_path] = ( + self._obtain_trackable_prim_view(target_prim_path) + ) + + # throw an error if no meshes are found + if all([target_cfg.prim_expr not in multi_mesh_ids for target_cfg in self._raycast_targets_cfg]): + raise RuntimeError( + f"No meshes found for ray-casting! Please check the mesh prim paths: {self.cfg.mesh_prim_paths}" + ) + + total_n_meshes_per_env = sum(self._num_meshes_per_env.values()) + self._mesh_positions_w = torch.zeros(self._num_envs, total_n_meshes_per_env, 3, device=self.device) + self._mesh_orientations_w = torch.zeros(self._num_envs, total_n_meshes_per_env, 4, device=self.device) + + # Update the mesh positions and rotations + mesh_idx = 0 + for target_cfg in self._raycast_targets_cfg: + n_meshes = self._num_meshes_per_env[target_cfg.prim_expr] + + # update position of the target meshes + pos_w, ori_w = [], [] + for prim in sim_utils.find_matching_prims(target_cfg.prim_expr): + translation, quat = sim_utils.resolve_prim_pose(prim) + pos_w.append(translation) + ori_w.append(quat) + pos_w = torch.tensor(pos_w, device=self.device, dtype=torch.float32).view(-1, n_meshes, 3) + ori_w = torch.tensor(ori_w, device=self.device, dtype=torch.float32).view(-1, n_meshes, 4) + + self._mesh_positions_w[:, mesh_idx : mesh_idx + n_meshes] = pos_w + self._mesh_orientations_w[:, mesh_idx : mesh_idx + n_meshes] = ori_w + mesh_idx += n_meshes + + # flatten the list of meshes that are included in mesh_prim_paths of the specific ray caster + multi_mesh_ids_flattened = [] + for env_idx in range(self._num_envs): + meshes_in_env = [] + for target_cfg in self._raycast_targets_cfg: + meshes_in_env.extend(multi_mesh_ids[target_cfg.prim_expr][env_idx]) + multi_mesh_ids_flattened.append(meshes_in_env) + + self._mesh_views = [ + self.mesh_views[target_cfg.prim_expr] if target_cfg.track_mesh_transforms else None + for target_cfg in self._raycast_targets_cfg + ] + + # save a warp array with mesh ids that is passed to the raycast function + self._mesh_ids_wp = wp.array2d(multi_mesh_ids_flattened, dtype=wp.uint64, device=self.device) + + def _initialize_rays_impl(self): + super()._initialize_rays_impl() + if self.cfg.update_mesh_ids: + self._data.ray_mesh_ids = torch.zeros( + self._num_envs, self.num_rays, 1, device=self.device, dtype=torch.int16 + ) + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data. + + Args: + env_ids: The environment ids to update. + """ + + self._update_ray_infos(env_ids) + + # Update the mesh positions and rotations + mesh_idx = 0 + for view, target_cfg in zip(self._mesh_views, self._raycast_targets_cfg): + if not target_cfg.track_mesh_transforms: + mesh_idx += self._num_meshes_per_env[target_cfg.prim_expr] + continue + + # update position of the target meshes + pos_w, ori_w = obtain_world_pose_from_view(view, None) + pos_w = pos_w.squeeze(0) if len(pos_w.shape) == 3 else pos_w + ori_w = ori_w.squeeze(0) if len(ori_w.shape) == 3 else ori_w + + if target_cfg.prim_expr in MultiMeshRayCaster.mesh_offsets: + pos_offset, ori_offset = MultiMeshRayCaster.mesh_offsets[target_cfg.prim_expr] + pos_w -= pos_offset + ori_w = quat_mul(ori_offset.expand(ori_w.shape[0], -1), ori_w) + + count = view.count + if count != 1: # Mesh is not global, i.e. we have different meshes for each env + count = count // self._num_envs + pos_w = pos_w.view(self._num_envs, count, 3) + ori_w = ori_w.view(self._num_envs, count, 4) + + self._mesh_positions_w[:, mesh_idx : mesh_idx + count] = pos_w + self._mesh_orientations_w[:, mesh_idx : mesh_idx + count] = ori_w + mesh_idx += count + + self._data.ray_hits_w[env_ids], _, _, _, mesh_ids = raycast_dynamic_meshes( + self._ray_starts_w[env_ids], + self._ray_directions_w[env_ids], + mesh_ids_wp=self._mesh_ids_wp, # list with shape num_envs x num_meshes_per_env + max_dist=self.cfg.max_distance, + mesh_positions_w=self._mesh_positions_w[env_ids], + mesh_orientations_w=self._mesh_orientations_w[env_ids], + return_mesh_id=self.cfg.update_mesh_ids, + ) + + if self.cfg.update_mesh_ids: + self._data.ray_mesh_ids[env_ids] = mesh_ids + + def __del__(self): + super().__del__() + if RayCaster._instance_count == 0: + MultiMeshRayCaster.mesh_offsets.clear() + MultiMeshRayCaster.mesh_views.clear() + + +""" +Helper functions +""" + + +def _registered_points_idx(points: np.ndarray, registered_points: list[np.ndarray | None]) -> int: + """Check if the points are already registered in the list of registered points. + + Args: + points: The points to check. + registered_points: The list of registered points. + + Returns: + The index of the registered points if found, otherwise -1. + """ + for idx, reg_points in enumerate(registered_points): + if reg_points is None: + continue + if reg_points.shape == points.shape and (reg_points == points).all(): + return idx + return -1 diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera.py b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera.py new file mode 100644 index 000000000000..970860fa50ae --- /dev/null +++ b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera.py @@ -0,0 +1,221 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.utils.warp import raycast_dynamic_meshes + +from .multi_mesh_ray_caster import MultiMeshRayCaster +from .multi_mesh_ray_caster_camera_data import MultiMeshRayCasterCameraData +from .ray_cast_utils import obtain_world_pose_from_view +from .ray_caster_camera import RayCasterCamera + +if TYPE_CHECKING: + from .multi_mesh_ray_caster_camera_cfg import MultiMeshRayCasterCameraCfg + + +class MultiMeshRayCasterCamera(RayCasterCamera, MultiMeshRayCaster): + """A multi-mesh ray-casting camera sensor. + + The ray-caster camera uses a set of rays to get the distances to meshes in the scene. The rays are + defined in the sensor's local coordinate frame. The sensor has the same interface as the + :class:`isaaclab.sensors.Camera` that implements the camera class through USD camera prims. + However, this class provides a faster image generation. The sensor converts meshes from the list of + primitive paths provided in the configuration to Warp meshes. The camera then ray-casts against these + Warp meshes only. + + Currently, only the following annotators are supported: + + - ``"distance_to_camera"``: An image containing the distance to camera optical center. + - ``"distance_to_image_plane"``: An image containing distances of 3D points from camera plane along camera's z-axis. + - ``"normals"``: An image containing the local surface normal vectors at each pixel. + """ + + cfg: MultiMeshRayCasterCameraCfg + """The configuration parameters.""" + + def __init__(self, cfg: MultiMeshRayCasterCameraCfg): + """Initializes the camera object. + + Args: + cfg: The configuration parameters. + + Raises: + ValueError: If the provided data types are not supported by the ray-caster camera. + """ + self._check_supported_data_types(cfg) + # initialize base class + MultiMeshRayCaster.__init__(self, cfg) + # create empty variables for storing output data + self._data = MultiMeshRayCasterCameraData() + + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"Multi-Mesh Ray-Caster-Camera @ '{self.cfg.prim_path}': \n" + f"\tview type : {self._view.__class__}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tnumber of meshes : {len(MultiMeshRayCaster.meshes)}\n" + f"\tnumber of sensors : {self._view.count}\n" + f"\tnumber of rays/sensor: {self.num_rays}\n" + f"\ttotal number of rays : {self.num_rays * self._view.count}\n" + f"\timage shape : {self.image_shape}" + ) + + """ + Implementation. + """ + + def _initialize_warp_meshes(self): + MultiMeshRayCaster._initialize_warp_meshes(self) + + def _create_buffers(self): + super()._create_buffers() + self._data.image_mesh_ids = torch.zeros( + self._num_envs, *self.image_shape, 1, device=self.device, dtype=torch.int16 + ) + + def _initialize_rays_impl(self): + # Create all indices buffer + self._ALL_INDICES = torch.arange(self._view.count, device=self._device, dtype=torch.long) + # Create frame count buffer + self._frame = torch.zeros(self._view.count, device=self._device, dtype=torch.long) + # create buffers + self._create_buffers() + # compute intrinsic matrices + self._compute_intrinsic_matrices() + # compute ray stars and directions + self.ray_starts, self.ray_directions = self.cfg.pattern_cfg.func( + self.cfg.pattern_cfg, self._data.intrinsic_matrices, self._device + ) + self.num_rays = self.ray_directions.shape[1] + # create buffer to store ray hits + self.ray_hits_w = torch.zeros(self._view.count, self.num_rays, 3, device=self._device) + # set offsets + quat_w = math_utils.convert_camera_frame_orientation_convention( + torch.tensor([self.cfg.offset.rot], device=self._device), origin=self.cfg.offset.convention, target="world" + ) + self._offset_quat = quat_w.repeat(self._view.count, 1) + self._offset_pos = torch.tensor(list(self.cfg.offset.pos), device=self._device).repeat(self._view.count, 1) + + self._data.quat_w = torch.zeros(self._view.count, 4, device=self.device) + self._data.pos_w = torch.zeros(self._view.count, 3, device=self.device) + + self._ray_starts_w = torch.zeros(self._view.count, self.num_rays, 3, device=self.device) + self._ray_directions_w = torch.zeros(self._view.count, self.num_rays, 3, device=self.device) + + def _update_ray_infos(self, env_ids: Sequence[int]): + """Updates the ray information buffers.""" + + # compute poses from current view + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids) + pos_w, quat_w = math_utils.combine_frame_transforms( + pos_w, quat_w, self._offset_pos[env_ids], self._offset_quat[env_ids] + ) + # update the data + self._data.pos_w[env_ids] = pos_w + self._data.quat_w_world[env_ids] = quat_w + self._data.quat_w_ros[env_ids] = quat_w + + # note: full orientation is considered + ray_starts_w = math_utils.quat_apply(quat_w.repeat(1, self.num_rays), self.ray_starts[env_ids]) + ray_starts_w += pos_w.unsqueeze(1) + ray_directions_w = math_utils.quat_apply(quat_w.repeat(1, self.num_rays), self.ray_directions[env_ids]) + + self._ray_starts_w[env_ids] = ray_starts_w + self._ray_directions_w[env_ids] = ray_directions_w + + def _update_buffers_impl(self, env_ids: Sequence[int] | torch.Tensor | None): + """Fills the buffers of the sensor data.""" + self._update_ray_infos(env_ids) + + # increment frame count + if env_ids is None: + env_ids = torch.arange(self._num_envs, device=self.device) + elif not isinstance(env_ids, torch.Tensor): + env_ids = torch.tensor(env_ids, device=self.device) + + self._frame[env_ids] += 1 + + # Update the mesh positions and rotations + mesh_idx = 0 + for view, target_cfg in zip(self._mesh_views, self._raycast_targets_cfg): + if not target_cfg.track_mesh_transforms: + mesh_idx += self._num_meshes_per_env[target_cfg.prim_expr] + continue + + # update position of the target meshes + pos_w, ori_w = obtain_world_pose_from_view(view, None) + pos_w = pos_w.squeeze(0) if len(pos_w.shape) == 3 else pos_w + ori_w = ori_w.squeeze(0) if len(ori_w.shape) == 3 else ori_w + + if target_cfg.prim_expr in MultiMeshRayCaster.mesh_offsets: + pos_offset, ori_offset = MultiMeshRayCaster.mesh_offsets[target_cfg.prim_expr] + pos_w -= pos_offset + ori_w = math_utils.quat_mul(ori_offset.expand(ori_w.shape[0], -1), ori_w) + + count = view.count + if count != 1: # Mesh is not global, i.e. we have different meshes for each env + count = count // self._num_envs + pos_w = pos_w.view(self._num_envs, count, 3) + ori_w = ori_w.view(self._num_envs, count, 4) + + self._mesh_positions_w[:, mesh_idx : mesh_idx + count] = pos_w + self._mesh_orientations_w[:, mesh_idx : mesh_idx + count] = ori_w + mesh_idx += count + + # ray cast and store the hits + self.ray_hits_w[env_ids], ray_depth, ray_normal, _, ray_mesh_ids = raycast_dynamic_meshes( + self._ray_starts_w[env_ids], + self._ray_directions_w[env_ids], + mesh_ids_wp=self._mesh_ids_wp, # list with shape num_envs x num_meshes_per_env + max_dist=self.cfg.max_distance, + mesh_positions_w=self._mesh_positions_w[env_ids], + mesh_orientations_w=self._mesh_orientations_w[env_ids], + return_distance=any( + [name in self.cfg.data_types for name in ["distance_to_image_plane", "distance_to_camera"]] + ), + return_normal="normals" in self.cfg.data_types, + return_mesh_id=self.cfg.update_mesh_ids, + ) + + # update output buffers + if "distance_to_image_plane" in self.cfg.data_types: + # note: data is in camera frame so we only take the first component (z-axis of camera frame) + distance_to_image_plane = ( + math_utils.quat_apply( + math_utils.quat_inv(self._data.quat_w_world[env_ids]).repeat(1, self.num_rays), + (ray_depth[:, :, None] * self._ray_directions_w[env_ids]), + ) + )[:, :, 0] + # apply the maximum distance after the transformation + if self.cfg.depth_clipping_behavior == "max": + distance_to_image_plane = torch.clip(distance_to_image_plane, max=self.cfg.max_distance) + distance_to_image_plane[torch.isnan(distance_to_image_plane)] = self.cfg.max_distance + elif self.cfg.depth_clipping_behavior == "zero": + distance_to_image_plane[distance_to_image_plane > self.cfg.max_distance] = 0.0 + distance_to_image_plane[torch.isnan(distance_to_image_plane)] = 0.0 + self._data.output["distance_to_image_plane"][env_ids] = distance_to_image_plane.view( + -1, *self.image_shape, 1 + ) + + if "distance_to_camera" in self.cfg.data_types: + if self.cfg.depth_clipping_behavior == "max": + ray_depth = torch.clip(ray_depth, max=self.cfg.max_distance) + elif self.cfg.depth_clipping_behavior == "zero": + ray_depth[ray_depth > self.cfg.max_distance] = 0.0 + self._data.output["distance_to_camera"][env_ids] = ray_depth.view(-1, *self.image_shape, 1) + + if "normals" in self.cfg.data_types: + self._data.output["normals"][env_ids] = ray_normal.view(-1, *self.image_shape, 3) + + if self.cfg.update_mesh_ids: + self._data.image_mesh_ids[env_ids] = ray_mesh_ids.view(-1, *self.image_shape, 1) diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_cfg.py b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_cfg.py new file mode 100644 index 000000000000..45df51ce6d80 --- /dev/null +++ b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_cfg.py @@ -0,0 +1,35 @@ +# 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 + +"""Configuration for the ray-cast camera sensor.""" + +import logging + +from isaaclab.utils import configclass + +from .multi_mesh_ray_caster_camera import MultiMeshRayCasterCamera +from .multi_mesh_ray_caster_cfg import MultiMeshRayCasterCfg +from .ray_caster_camera_cfg import RayCasterCameraCfg + +# import logger +logger = logging.getLogger(__name__) + + +@configclass +class MultiMeshRayCasterCameraCfg(RayCasterCameraCfg, MultiMeshRayCasterCfg): + """Configuration for the multi-mesh ray-cast camera sensor.""" + + class_type: type = MultiMeshRayCasterCamera + + def __post_init__(self): + super().__post_init__() + + # Camera only supports 'base' ray alignment. Ensure this is set correctly. + if self.ray_alignment != "base": + logger.warning( + "Ray alignment for MultiMeshRayCasterCameraCfg only supports 'base' alignment. Overriding from" + f"'{self.ray_alignment}' to 'base'." + ) + self.ray_alignment = "base" diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_data.py b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_data.py new file mode 100644 index 000000000000..d2f26abdbf47 --- /dev/null +++ b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_data.py @@ -0,0 +1,23 @@ +# 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 + +"""Data container for the multi-mesh ray-cast camera sensor.""" + +import torch + +from isaaclab.sensors.camera import CameraData + +from .ray_caster_data import RayCasterData + + +class MultiMeshRayCasterCameraData(CameraData, RayCasterData): + """Data container for the multi-mesh ray-cast sensor.""" + + image_mesh_ids: torch.Tensor = None + """The mesh ids of the image pixels. + + Shape is (N, H, W, 1), where N is the number of sensors, H and W are the height and width of the image, + and 1 is the number of mesh ids per pixel. + """ diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_cfg.py b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_cfg.py new file mode 100644 index 000000000000..f5393920162a --- /dev/null +++ b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_cfg.py @@ -0,0 +1,71 @@ +# 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 + + +"""Configuration for the ray-cast sensor.""" + +from dataclasses import MISSING + +from isaaclab.utils import configclass + +from .multi_mesh_ray_caster import MultiMeshRayCaster +from .ray_caster_cfg import RayCasterCfg + + +@configclass +class MultiMeshRayCasterCfg(RayCasterCfg): + """Configuration for the multi-mesh ray-cast sensor.""" + + @configclass + class RaycastTargetCfg: + """Configuration for different ray-cast targets.""" + + prim_expr: str = MISSING + """The regex to specify the target prim to ray cast against.""" + + is_shared: bool = False + """Whether the target prim is assumed to be the same mesh across all environments. Defaults to False. + + If True, only the first mesh is read and then reused for all environments, rather than re-parsed. + This provides a startup performance boost when there are many environments that all use the same asset. + + .. note:: + If :attr:`MultiMeshRayCasterCfg.reference_meshes` is False, this flag has no effect. + """ + + merge_prim_meshes: bool = True + """Whether to merge the parsed meshes for a prim that contains multiple meshes. Defaults to True. + + This will create a new mesh that combines all meshes in the parsed prim. The raycast hits mesh IDs + will then refer to the single merged mesh. + """ + + track_mesh_transforms: bool = True + """Whether the mesh transformations should be tracked. Defaults to True. + + .. note:: + Not tracking the mesh transformations is recommended when the meshes are static to increase performance. + """ + + class_type: type = MultiMeshRayCaster + + mesh_prim_paths: list[str | RaycastTargetCfg] = MISSING + """The list of mesh primitive paths to ray cast against. + + If an entry is a string, it is internally converted to :class:`RaycastTargetCfg` with + :attr:`~RaycastTargetCfg.track_mesh_transforms` disabled. These settings ensure backwards compatibility + with the default raycaster. + """ + + update_mesh_ids: bool = False + """Whether to update the mesh ids of the ray hits in the :attr:`data` container.""" + + reference_meshes: bool = True + """Whether to reference duplicated meshes instead of loading each one separately into memory. + Defaults to True. + + When enabled, the raycaster parses all meshes in all environments, but reuses references + for duplicates instead of storing multiple copies. This reduces memory footprint. + """ diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_data.py b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_data.py new file mode 100644 index 000000000000..b9ae187591be --- /dev/null +++ b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_data.py @@ -0,0 +1,22 @@ +# 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 + + +"""Data container for the multi-mesh ray-cast sensor.""" + +import torch + +from .ray_caster_data import RayCasterData + + +class MultiMeshRayCasterData(RayCasterData): + """Data container for the multi-mesh ray-cast sensor.""" + + ray_mesh_ids: torch.Tensor = None + """The mesh ids of the ray hits. + + Shape is (N, B, 1), where N is the number of sensors, B is the number of rays + in the scan pattern per sensor. + """ diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/patterns/__init__.py b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/__init__.py index 383460ae8e63..d43f5437ce01 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/patterns/__init__.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py index 2a6a438d1781..cae68833c4f2 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,9 +6,10 @@ from __future__ import annotations import math -import torch from typing import TYPE_CHECKING +import torch + if TYPE_CHECKING: from . import patterns_cfg @@ -153,7 +154,9 @@ def lidar_pattern(cfg: patterns_cfg.LidarPatternCfg, device: str) -> tuple[torch up_to = None # Horizontal angles - num_horizontal_angles = math.ceil((cfg.horizontal_fov_range[1] - cfg.horizontal_fov_range[0]) / cfg.horizontal_res) + num_horizontal_angles = ( + math.ceil((cfg.horizontal_fov_range[1] - cfg.horizontal_fov_range[0]) / cfg.horizontal_res) + 1 + ) horizontal_angles = torch.linspace(cfg.horizontal_fov_range[0], cfg.horizontal_fov_range[1], num_horizontal_angles)[ :up_to ] diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns_cfg.py b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns_cfg.py index 374cb91f7180..f50ba272b708 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns_cfg.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,12 @@ from __future__ import annotations -import torch from collections.abc import Callable, Sequence from dataclasses import MISSING from typing import Literal +import torch + from isaaclab.utils import configclass from . import patterns @@ -137,8 +138,8 @@ def from_intrinsic_matrix( 0 & 0 & 1 \end{bmatrix}, - where :math:`f_x` and :math:`f_y` are the focal length along x and y direction, while :math:`c_x` and :math:`c_y` are the - principle point offsets along x and y direction respectively. + where :math:`f_x` and :math:`f_y` are the focal length along x and y direction, while + :math:`c_x` and :math:`c_y` are the principle point offsets along x and y direction, respectively. Args: intrinsic_matrix: Intrinsic matrix of the camera in row-major format. diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/ray_cast_utils.py b/source/isaaclab/isaaclab/sensors/ray_caster/ray_cast_utils.py new file mode 100644 index 000000000000..543276e8ea2a --- /dev/null +++ b/source/isaaclab/isaaclab/sensors/ray_caster/ray_cast_utils.py @@ -0,0 +1,51 @@ +# 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 + +"""Utility functions for ray-cast sensors.""" + +from __future__ import annotations + +import torch + +import omni.physics.tensors.impl.api as physx + +from isaaclab.sim.views import XformPrimView +from isaaclab.utils.math import convert_quat + + +def obtain_world_pose_from_view( + physx_view: XformPrimView | physx.ArticulationView | physx.RigidBodyView, + env_ids: torch.Tensor, + clone: bool = False, +) -> tuple[torch.Tensor, torch.Tensor]: + """Get the world poses of the prim referenced by the prim view. + + Args: + physx_view: The prim view to get the world poses from. + env_ids: The environment ids of the prims to get the world poses for. + clone: Whether to clone the returned tensors (default: False). + + Returns: + A tuple containing the world positions and orientations of the prims. + Orientation is in (w, x, y, z) format. + + Raises: + NotImplementedError: If the prim view is not of the supported type. + """ + if isinstance(physx_view, XformPrimView): + pos_w, quat_w = physx_view.get_world_poses(env_ids) + elif isinstance(physx_view, physx.ArticulationView): + pos_w, quat_w = physx_view.get_root_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = convert_quat(quat_w, to="wxyz") + elif isinstance(physx_view, physx.RigidBodyView): + pos_w, quat_w = physx_view.get_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = convert_quat(quat_w, to="wxyz") + else: + raise NotImplementedError(f"Cannot get world poses for prim view of type '{type(physx_view)}'.") + + if clone: + return pos_w.clone(), quat_w.clone() + else: + return pos_w, quat_w diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster.py b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster.py index 8e2f6541fe99..e6735a9f4819 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster.py @@ -1,36 +1,41 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import numpy as np +import logging import re -import torch from collections.abc import Sequence -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar -import omni.log -import omni.physics.tensors.impl.api as physx +import numpy as np +import torch import warp as wp -from isaacsim.core.prims import XFormPrim + +import omni from isaacsim.core.simulation_manager import SimulationManager from pxr import UsdGeom, UsdPhysics import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils from isaaclab.markers import VisualizationMarkers +from isaaclab.sim.views import XformPrimView from isaaclab.terrains.trimesh.utils import make_plane -from isaaclab.utils.math import convert_quat, quat_apply, quat_apply_yaw +from isaaclab.utils.math import quat_apply, quat_apply_yaw from isaaclab.utils.warp import convert_to_warp_mesh, raycast_mesh from ..sensor_base import SensorBase +from .ray_cast_utils import obtain_world_pose_from_view from .ray_caster_data import RayCasterData if TYPE_CHECKING: from .ray_caster_cfg import RayCasterCfg +# import logger +logger = logging.getLogger(__name__) + class RayCaster(SensorBase): """A ray-casting sensor. @@ -51,12 +56,21 @@ class RayCaster(SensorBase): cfg: RayCasterCfg """The configuration parameters.""" + # Class variables to share meshes across instances + meshes: ClassVar[dict[str, wp.Mesh]] = {} + """A dictionary to store warp meshes for raycasting, shared across all instances. + + The keys correspond to the prim path for the meshes, and values are the corresponding warp Mesh objects.""" + _instance_count: ClassVar[int] = 0 + """A counter to track the number of RayCaster instances, used to manage class variable lifecycle.""" + def __init__(self, cfg: RayCasterCfg): """Initializes the ray-caster object. Args: cfg: The configuration parameters. """ + RayCaster._instance_count += 1 # check if sensor path is valid # note: currently we do not handle environment indices if there is a regex pattern in the leaf # For example, if the prim path is "/World/Sensor_[1,2]". @@ -64,15 +78,13 @@ def __init__(self, cfg: RayCasterCfg): sensor_path_is_regex = re.match(r"^[a-zA-Z0-9/_]+$", sensor_path) is None if sensor_path_is_regex: raise RuntimeError( - f"Invalid prim path for the ray-caster sensor: {self.cfg.prim_path}." + f"Invalid prim path for the ray-caster sensor: {cfg.prim_path}." "\n\tHint: Please ensure that the prim path does not contain any regex patterns in the leaf." ) # Initialize base class super().__init__(cfg) # Create empty variables for storing output data self._data = RayCasterData() - # the warp meshes used for raycasting. - self.meshes: dict[str, wp.Mesh] = {} def __str__(self) -> str: """Returns: A string containing information about the instance.""" @@ -80,7 +92,7 @@ def __str__(self) -> str: f"Ray-caster @ '{self.cfg.prim_path}': \n" f"\tview type : {self._view.__class__}\n" f"\tupdate period (s) : {self.cfg.update_period}\n" - f"\tnumber of meshes : {len(self.meshes)}\n" + f"\tnumber of meshes : {len(RayCaster.meshes)}\n" f"\tnumber of sensors : {self._view.count}\n" f"\tnumber of rays/sensor: {self.num_rays}\n" f"\ttotal number of rays : {self.num_rays * self._view.count}" @@ -131,28 +143,16 @@ def reset(self, env_ids: Sequence[int] | None = None): def _initialize_impl(self): super()._initialize_impl() # obtain global simulation view + self._physics_sim_view = SimulationManager.get_physics_sim_view() - # check if the prim at path is an articulated or rigid prim - # we do this since for physics-based view classes we can access their data directly - # otherwise we need to use the xform view class which is slower - found_supported_prim_class = False prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) if prim is None: - raise RuntimeError(f"Failed to find a prim at path expression: {self.cfg.prim_path}") - # create view based on the type of prim - if prim.HasAPI(UsdPhysics.ArticulationRootAPI): - self._view = self._physics_sim_view.create_articulation_view(self.cfg.prim_path.replace(".*", "*")) - found_supported_prim_class = True - elif prim.HasAPI(UsdPhysics.RigidBodyAPI): - self._view = self._physics_sim_view.create_rigid_body_view(self.cfg.prim_path.replace(".*", "*")) - found_supported_prim_class = True - else: - self._view = XFormPrim(self.cfg.prim_path, reset_xform_properties=False) - found_supported_prim_class = True - omni.log.warn(f"The prim at path {prim.GetPath().pathString} is not a physics prim! Using XFormPrim.") - # check if prim view class is found - if not found_supported_prim_class: - raise RuntimeError(f"Failed to find a valid prim view class for the prim paths: {self.cfg.prim_path}") + available_prims = ",".join([str(p.GetPath()) for p in sim_utils.get_current_stage().Traverse()]) + raise RuntimeError( + f"Failed to find a prim at path expression: {self.cfg.prim_path}. Available prims: {available_prims}" + ) + + self._view, self._offset = self._obtain_trackable_prim_view(self.cfg.prim_path) # load the meshes by parsing the stage self._initialize_warp_meshes() @@ -168,6 +168,10 @@ def _initialize_warp_meshes(self): # read prims to ray-cast for mesh_prim_path in self.cfg.mesh_prim_paths: + # check if mesh already casted into warp mesh + if mesh_prim_path in RayCaster.meshes: + continue + # check if the prim is a plane - handle PhysX plane as a special case # if a plane exists then we need to create an infinite mesh that is a plane mesh_prim = sim_utils.get_first_matching_child_prim( @@ -192,19 +196,19 @@ def _initialize_warp_meshes(self): indices = np.asarray(mesh_prim.GetFaceVertexIndicesAttr().Get()) wp_mesh = convert_to_warp_mesh(points, indices, device=self.device) # print info - omni.log.info( + logger.info( f"Read mesh prim: {mesh_prim.GetPath()} with {len(points)} vertices and {len(indices)} faces." ) else: mesh = make_plane(size=(2e6, 2e6), height=0.0, center_zero=True) wp_mesh = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=self.device) # print info - omni.log.info(f"Created infinite plane mesh prim: {mesh_prim.GetPath()}.") + logger.info(f"Created infinite plane mesh prim: {mesh_prim.GetPath()}.") # add the warp mesh to the list - self.meshes[mesh_prim_path] = wp_mesh + RayCaster.meshes[mesh_prim_path] = wp_mesh # throw an error if no meshes are found - if all([mesh_prim_path not in self.meshes for mesh_prim_path in self.cfg.mesh_prim_paths]): + if all([mesh_prim_path not in RayCaster.meshes for mesh_prim_path in self.cfg.mesh_prim_paths]): raise RuntimeError( f"No meshes found for ray-casting! Please check the mesh prim paths: {self.cfg.mesh_prim_paths}" ) @@ -225,26 +229,19 @@ def _initialize_rays_impl(self): self.drift = torch.zeros(self._view.count, 3, device=self.device) self.ray_cast_drift = torch.zeros(self._view.count, 3, device=self.device) # fill the data buffer - self._data.pos_w = torch.zeros(self._view.count, 3, device=self._device) - self._data.quat_w = torch.zeros(self._view.count, 4, device=self._device) - self._data.ray_hits_w = torch.zeros(self._view.count, self.num_rays, 3, device=self._device) - - def _update_buffers_impl(self, env_ids: Sequence[int]): - """Fills the buffers of the sensor data.""" - # obtain the poses of the sensors - if isinstance(self._view, XFormPrim): - pos_w, quat_w = self._view.get_world_poses(env_ids) - elif isinstance(self._view, physx.ArticulationView): - pos_w, quat_w = self._view.get_root_transforms()[env_ids].split([3, 4], dim=-1) - quat_w = convert_quat(quat_w, to="wxyz") - elif isinstance(self._view, physx.RigidBodyView): - pos_w, quat_w = self._view.get_transforms()[env_ids].split([3, 4], dim=-1) - quat_w = convert_quat(quat_w, to="wxyz") - else: - raise RuntimeError(f"Unsupported view type: {type(self._view)}") - # note: we clone here because we are read-only operations - pos_w = pos_w.clone() - quat_w = quat_w.clone() + self._data.pos_w = torch.zeros(self._view.count, 3, device=self.device) + self._data.quat_w = torch.zeros(self._view.count, 4, device=self.device) + self._data.ray_hits_w = torch.zeros(self._view.count, self.num_rays, 3, device=self.device) + self._ray_starts_w = torch.zeros(self._view.count, self.num_rays, 3, device=self.device) + self._ray_directions_w = torch.zeros(self._view.count, self.num_rays, 3, device=self.device) + + def _update_ray_infos(self, env_ids: Sequence[int]): + """Updates the ray information buffers.""" + + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids) + pos_w, quat_w = math_utils.combine_frame_transforms( + pos_w, quat_w, self._offset[0][env_ids], self._offset[1][env_ids] + ) # apply drift to ray starting position in world frame pos_w += self.drift[env_ids] # store the poses @@ -265,7 +262,7 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): self.cfg.ray_alignment = "base" msg += " Setting ray_alignment to 'base'." # log the warning - omni.log.warn(msg) + logger.warning(msg) # ray cast based on the sensor poses if self.cfg.ray_alignment == "world": # apply horizontal drift to ray starting position in ray caster frame @@ -291,13 +288,20 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): else: raise RuntimeError(f"Unsupported ray_alignment type: {self.cfg.ray_alignment}.") + self._ray_starts_w[env_ids] = ray_starts_w + self._ray_directions_w[env_ids] = ray_directions_w + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data.""" + self._update_ray_infos(env_ids) + # ray cast and store the hits # TODO: Make this work for multiple meshes? self._data.ray_hits_w[env_ids] = raycast_mesh( - ray_starts_w, - ray_directions_w, + self._ray_starts_w[env_ids], + self._ray_directions_w[env_ids], max_dist=self.cfg.max_distance, - mesh=self.meshes[self.cfg.mesh_prim_paths[0]], + mesh=RayCaster.meshes[self.cfg.mesh_prim_paths[0]], )[0] # apply vertical drift to ray starting position in ray caster frame @@ -316,12 +320,97 @@ def _set_debug_vis_impl(self, debug_vis: bool): self.ray_visualizer.set_visibility(False) def _debug_vis_callback(self, event): + if self._data.ray_hits_w is None: + return # remove possible inf values viz_points = self._data.ray_hits_w.reshape(-1, 3) viz_points = viz_points[~torch.any(torch.isinf(viz_points), dim=1)] - # show ray hit positions + self.ray_visualizer.visualize(viz_points) + """ + Internal Helpers. + """ + + def _obtain_trackable_prim_view( + self, target_prim_path: str + ) -> tuple[XformPrimView | any, tuple[torch.Tensor, torch.Tensor]]: + """Obtain a prim view that can be used to track the pose of the parget prim. + + The target prim path is a regex expression that matches one or more mesh prims. While we can track its + pose directly using XFormPrim, this is not efficient and can be slow. Instead, we create a prim view + using the physics simulation view, which provides a more efficient way to track the pose of the mesh prims. + + The function additionally resolves the relative pose between the mesh and its corresponding physics prim. + This is especially useful if the mesh is not directly parented to the physics prim. + + Args: + target_prim_path: The target prim path to obtain the prim view for. + + Returns: + A tuple containing: + + - An XFormPrim or a physics prim view (ArticulationView or RigidBodyView). + - A tuple containing the positions and orientations of the mesh prims in the physics prim frame. + + """ + + mesh_prim = sim_utils.find_first_matching_prim(target_prim_path) + current_prim = mesh_prim + current_path_expr = target_prim_path + + prim_view = None + + while prim_view is None: + # TODO: Need to handle the case where API is present but it is disabled + if current_prim.HasAPI(UsdPhysics.ArticulationRootAPI): + prim_view = self._physics_sim_view.create_articulation_view(current_path_expr.replace(".*", "*")) + logger.info(f"Created articulation view for mesh prim at path: {target_prim_path}") + break + + # TODO: Need to handle the case where API is present but it is disabled + if current_prim.HasAPI(UsdPhysics.RigidBodyAPI): + prim_view = self._physics_sim_view.create_rigid_body_view(current_path_expr.replace(".*", "*")) + logger.info(f"Created rigid body view for mesh prim at path: {target_prim_path}") + break + + new_root_prim = current_prim.GetParent() + current_path_expr = current_path_expr.rsplit("/", 1)[0] + if not new_root_prim.IsValid(): + prim_view = XformPrimView(target_prim_path, device=self._device, stage=self.stage) + current_path_expr = target_prim_path + logger.warning( + f"The prim at path {target_prim_path} which is used for raycasting is not a physics prim." + " Defaulting to XFormPrim. \n The pose of the mesh will most likely not" + " be updated correctly when running in headless mode and position lookups will be much slower. \n" + " If possible, ensure that the mesh or its parent is a physics prim (rigid body or articulation)." + ) + break + + # switch the current prim to the parent prim + current_prim = new_root_prim + + # obtain the relative transforms between target prim and the view prims + mesh_prims = sim_utils.find_matching_prims(target_prim_path) + view_prims = sim_utils.find_matching_prims(current_path_expr) + if len(mesh_prims) != len(view_prims): + raise RuntimeError( + f"The number of mesh prims ({len(mesh_prims)}) does not match the number of physics prims" + f" ({len(view_prims)})Please specify the correct mesh and physics prim paths more" + " specifically in your target expressions." + ) + positions = [] + quaternions = [] + for mesh_prim, view_prim in zip(mesh_prims, view_prims): + pos, orientation = sim_utils.resolve_prim_pose(mesh_prim, view_prim) + positions.append(torch.tensor(pos, dtype=torch.float32, device=self.device)) + quaternions.append(torch.tensor(orientation, dtype=torch.float32, device=self.device)) + + positions = torch.stack(positions).to(device=self.device, dtype=torch.float32) + quaternions = torch.stack(quaternions).to(device=self.device, dtype=torch.float32) + + return prim_view, (positions, quaternions) + """ Internal simulation callbacks. """ @@ -332,3 +421,8 @@ def _invalidate_initialize_callback(self, event): super()._invalidate_initialize_callback(event) # set all existing views to None to invalidate them self._view = None + + def __del__(self): + RayCaster._instance_count -= 1 + if RayCaster._instance_count == 0: + RayCaster.meshes.clear() diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera.py b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera.py index 3bf8729b78bf..e930d3df1837 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera.py @@ -1,27 +1,31 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch +import logging from collections.abc import Sequence from typing import TYPE_CHECKING, ClassVar, Literal -import isaacsim.core.utils.stage as stage_utils -import omni.physics.tensors.impl.api as physx -from isaacsim.core.prims import XFormPrim +import torch + +from pxr import UsdGeom import isaaclab.utils.math as math_utils from isaaclab.sensors.camera import CameraData from isaaclab.utils.warp import raycast_mesh +from .ray_cast_utils import obtain_world_pose_from_view from .ray_caster import RayCaster if TYPE_CHECKING: from .ray_caster_camera_cfg import RayCasterCameraCfg +# import logger +logger = logging.getLogger(__name__) + class RayCasterCamera(RayCaster): """A ray-casting camera sensor. @@ -86,7 +90,7 @@ def __str__(self) -> str: f"Ray-Caster-Camera @ '{self.cfg.prim_path}': \n" f"\tview type : {self._view.__class__}\n" f"\tupdate period (s) : {self.cfg.update_period}\n" - f"\tnumber of meshes : {len(self.meshes)}\n" + f"\tnumber of meshes : {len(RayCaster.meshes)}\n" f"\tnumber of sensors : {self._view.count}\n" f"\tnumber of rays/sensor: {self.num_rays}\n" f"\ttotal number of rays : {self.num_rays * self._view.count}\n" @@ -143,11 +147,14 @@ def reset(self, env_ids: Sequence[int] | None = None): # reset the timestamps super().reset(env_ids) # resolve None - if env_ids is None: - env_ids = slice(None) + if env_ids is None or isinstance(env_ids, slice): + env_ids = self._ALL_INDICES # reset the data # note: this recomputation is useful if one performs events such as randomizations on the camera poses. - pos_w, quat_w = self._compute_camera_world_poses(env_ids) + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids, clone=True) + pos_w, quat_w = math_utils.combine_frame_transforms( + pos_w, quat_w, self._offset_pos[env_ids], self._offset_quat[env_ids] + ) self._data.pos_w[env_ids] = pos_w self._data.quat_w_world[env_ids] = quat_w # Reset the frame count @@ -184,11 +191,11 @@ def set_world_poses( RuntimeError: If the camera prim is not set. Need to call :meth:`initialize` method first. """ # resolve env_ids - if env_ids is None: + if env_ids is None or isinstance(env_ids, slice): env_ids = self._ALL_INDICES # get current positions - pos_w, quat_w = self._compute_view_world_poses(env_ids) + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids) if positions is not None: # transform to camera frame pos_offset_world_frame = positions - pos_w @@ -201,7 +208,10 @@ def set_world_poses( self._offset_quat[env_ids] = math_utils.quat_mul(math_utils.quat_inv(quat_w), quat_w_set) # update the data - pos_w, quat_w = self._compute_camera_world_poses(env_ids) + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids, clone=True) + pos_w, quat_w = math_utils.combine_frame_transforms( + pos_w, quat_w, self._offset_pos[env_ids], self._offset_quat[env_ids] + ) self._data.pos_w[env_ids] = pos_w self._data.quat_w_world[env_ids] = quat_w @@ -220,7 +230,7 @@ def set_world_poses_from_view( NotImplementedError: If the stage up-axis is not "Y" or "Z". """ # get up axis of current stage - up_axis = stage_utils.get_stage_up_axis() + up_axis = UsdGeom.GetStageUpAxis(self.stage) # camera position and rotation in opengl convention orientations = math_utils.quat_from_matrix( math_utils.create_rotation_matrix_from_view(eyes, targets, up_axis=up_axis, device=self._device) @@ -260,7 +270,10 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): self._frame[env_ids] += 1 # compute poses from current view - pos_w, quat_w = self._compute_camera_world_poses(env_ids) + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids, clone=True) + pos_w, quat_w = math_utils.combine_frame_transforms( + pos_w, quat_w, self._offset_pos[env_ids], self._offset_quat[env_ids] + ) # update the data self._data.pos_w[env_ids] = pos_w self._data.quat_w_world[env_ids] = quat_w @@ -280,7 +293,7 @@ def _update_buffers_impl(self, env_ids: Sequence[int]): self.ray_hits_w, ray_depth, ray_normal, _ = raycast_mesh( ray_starts_w, ray_directions_w, - mesh=self.meshes[self.cfg.mesh_prim_paths[0]], + mesh=RayCaster.meshes[self.cfg.mesh_prim_paths[0]], max_dist=1e6, return_distance=any( [name in self.cfg.data_types for name in ["distance_to_image_plane", "distance_to_camera"]] @@ -395,39 +408,49 @@ def _compute_intrinsic_matrices(self): def _compute_view_world_poses(self, env_ids: Sequence[int]) -> tuple[torch.Tensor, torch.Tensor]: """Obtains the pose of the view the camera is attached to in the world frame. + .. deprecated v2.3.1: + This function will be removed in a future release in favor of implementation + :meth:`obtain_world_pose_from_view`. + Returns: A tuple of the position (in meters) and quaternion (w, x, y, z). + + """ - # obtain the poses of the sensors - # note: clone arg doesn't exist for xform prim view so we need to do this manually - if isinstance(self._view, XFormPrim): - if isinstance(env_ids, slice): # catch the case where env_ids is a slice - env_ids = self._ALL_INDICES - pos_w, quat_w = self._view.get_world_poses(env_ids) - elif isinstance(self._view, physx.ArticulationView): - pos_w, quat_w = self._view.get_root_transforms()[env_ids].split([3, 4], dim=-1) - quat_w = math_utils.convert_quat(quat_w, to="wxyz") - elif isinstance(self._view, physx.RigidBodyView): - pos_w, quat_w = self._view.get_transforms()[env_ids].split([3, 4], dim=-1) - quat_w = math_utils.convert_quat(quat_w, to="wxyz") - else: - raise RuntimeError(f"Unsupported view type: {type(self._view)}") - # return the pose - return pos_w.clone(), quat_w.clone() + # deprecation + logger.warning( + "The function '_compute_view_world_poses' will be deprecated in favor of the util method" + " 'obtain_world_pose_from_view'. Please use 'obtain_world_pose_from_view' instead...." + ) + + return obtain_world_pose_from_view(self._view, env_ids, clone=True) def _compute_camera_world_poses(self, env_ids: Sequence[int]) -> tuple[torch.Tensor, torch.Tensor]: """Computes the pose of the camera in the world frame. This function applies the offset pose to the pose of the view the camera is attached to. + .. deprecated v2.3.1: + This function will be removed in a future release. Instead, use the code block below: + + .. code-block:: python + + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids, clone=True) + pos_w, quat_w = math_utils.combine_frame_transforms( + pos_w, quat_w, self._offset_pos[env_ids], self._offset_quat[env_ids] + ) + Returns: A tuple of the position (in meters) and quaternion (w, x, y, z) in "world" convention. """ - # get the pose of the view the camera is attached to - pos_w, quat_w = self._compute_view_world_poses(env_ids) - # apply offsets - # need to apply quat because offset relative to parent frame - pos_w += math_utils.quat_apply(quat_w, self._offset_pos[env_ids]) - quat_w = math_utils.quat_mul(quat_w, self._offset_quat[env_ids]) - return pos_w, quat_w + # deprecation + logger.warning( + "The function '_compute_camera_world_poses' will be deprecated in favor of the combination of methods" + " 'obtain_world_pose_from_view' and 'math_utils.combine_frame_transforms'. Please use" + " 'obtain_world_pose_from_view' and 'math_utils.combine_frame_transforms' instead...." + ) + + # get the pose of the view the camera is attached to + pos_w, quat_w = obtain_world_pose_from_view(self._view, env_ids, clone=True) + return math_utils.combine_frame_transforms(pos_w, quat_w, self._offset_pos[env_ids], self._offset_quat[env_ids]) diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera_cfg.py b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera_cfg.py index c90984e8d7f6..604c586adcc7 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera_cfg.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_camera_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -32,7 +32,8 @@ class OffsetCfg: convention: Literal["opengl", "ros", "world"] = "ros" """The convention in which the frame offset is applied. Defaults to "ros". - - ``"opengl"`` - forward axis: ``-Z`` - up axis: ``+Y`` - Offset is applied in the OpenGL (Usd.Camera) convention. + - ``"opengl"`` - forward axis: ``-Z`` - up axis: ``+Y`` - Offset is applied in the OpenGL (Usd.Camera) + convention. - ``"ros"`` - forward axis: ``+Z`` - up axis: ``-Y`` - Offset is applied in the ROS convention. - ``"world"`` - forward axis: ``+X`` - up axis: ``+Z`` - Offset is applied in the World Frame convention. diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_cfg.py b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_cfg.py index 4a4884e32a57..dbdebfad3a5e 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_cfg.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_cfg.py @@ -1,11 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Configuration for the ray-cast sensor.""" - from dataclasses import MISSING from typing import Literal @@ -66,10 +65,10 @@ class OffsetCfg: The options are: * ``base`` if the rays' starting positions and directions track the full root position and orientation. - * ``yaw`` if the rays' starting positions and directions track root position and only yaw component of orientation. - This is useful for ray-casting height maps. - * ``world`` if rays' starting positions and directions are always fixed. This is useful in combination with a mapping - package on the robot and querying ray-casts in a global frame. + * ``yaw`` if the rays' starting positions and directions track root position and only yaw component of + the orientation. This is useful for ray-casting height maps. + * ``world`` if rays' starting positions and directions are always fixed. This is useful in combination + with a mapping package on the robot and querying ray-casts in a global frame. """ pattern_cfg: PatternBaseCfg = MISSING diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_data.py b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_data.py index 975fa72eb5b6..d63e085e752f 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_data.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/ray_caster_data.py @@ -1,11 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from dataclasses import dataclass +import torch + @dataclass class RayCasterData: diff --git a/source/isaaclab/isaaclab/sensors/sensor_base.py b/source/isaaclab/isaaclab/sensors/sensor_base.py index aab993cc526b..4ece160bbe5a 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,18 +14,19 @@ import builtins import inspect import re -import torch import weakref from abc import ABC, abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING, Any +import torch + import omni.kit.app import omni.timeline from isaacsim.core.simulation_manager import IsaacEvents, SimulationManager -from isaacsim.core.utils.stage import get_current_stage import isaaclab.sim as sim_utils +from isaaclab.sim.utils.stage import get_current_stage if TYPE_CHECKING: from .sensor_base_cfg import SensorBaseCfg diff --git a/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py b/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py index 9b2884a21289..85875b2e4996 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/__init__.py b/source/isaaclab/isaaclab/sim/__init__.py index 3703132e5507..1dc920f4e100 100644 --- a/source/isaaclab/isaaclab/sim/__init__.py +++ b/source/isaaclab/isaaclab/sim/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -32,3 +32,4 @@ from .simulation_context import SimulationContext, build_simulation_context # noqa: F401, F403 from .spawners import * # noqa: F401, F403 from .utils import * # noqa: F401, F403 +from .views import * # noqa: F401, F403 diff --git a/source/isaaclab/isaaclab/sim/converters/__init__.py b/source/isaaclab/isaaclab/sim/converters/__init__.py index a8bccee7c03b..7503c53bdd85 100644 --- a/source/isaaclab/isaaclab/sim/converters/__init__.py +++ b/source/isaaclab/isaaclab/sim/converters/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py b/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py index 230d8e7eee57..11c200422391 100644 --- a/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py +++ b/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +40,8 @@ class AssetConverterBase(abc.ABC): .. note:: Changes to the parameters :obj:`AssetConverterBaseCfg.asset_path`, :obj:`AssetConverterBaseCfg.usd_dir`, and - :obj:`AssetConverterBaseCfg.usd_file_name` are not considered as modifications in the configuration instance that - trigger USD file re-generation. + :obj:`AssetConverterBaseCfg.usd_file_name` are not considered as modifications in the configuration instance + that trigger the USD file re-generation. """ diff --git a/source/isaaclab/isaaclab/sim/converters/asset_converter_base_cfg.py b/source/isaaclab/isaaclab/sim/converters/asset_converter_base_cfg.py index 20d7e35a3653..79bb8d17d41c 100644 --- a/source/isaaclab/isaaclab/sim/converters/asset_converter_base_cfg.py +++ b/source/isaaclab/isaaclab/sim/converters/asset_converter_base_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index a35167fad99d..4a79a908bab1 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -1,21 +1,24 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 asyncio +import logging import os import omni import omni.kit.commands -import omni.usd from isaacsim.core.utils.extensions import enable_extension from pxr import Gf, Tf, Usd, UsdGeom, UsdPhysics, UsdUtils from isaaclab.sim.converters.asset_converter_base import AssetConverterBase from isaaclab.sim.converters.mesh_converter_cfg import MeshConverterCfg from isaaclab.sim.schemas import schemas -from isaaclab.sim.utils import export_prim_to_file +from isaaclab.sim.utils import delete_prim, export_prim_to_file + +# import logger +logger = logging.getLogger(__name__) class MeshConverter(AssetConverterBase): @@ -87,7 +90,7 @@ def _convert_asset(self, cfg: MeshConverterCfg): # Correct the name to a valid identifier and update the basename mesh_file_basename_original = mesh_file_basename mesh_file_basename = Tf.MakeValidIdentifier(mesh_file_basename) - omni.log.warn( + logger.warning( f"Input file name '{mesh_file_basename_original}' is an invalid identifier for the mesh prim path." f" Renaming it to '{mesh_file_basename}' for the conversion." ) @@ -170,7 +173,7 @@ def _convert_asset(self, cfg: MeshConverterCfg): ) # Delete the original prim that will now be a reference geom_prim_path = geom_prim.GetPath().pathString - omni.kit.commands.execute("DeletePrims", paths=[geom_prim_path], stage=stage) + delete_prim(geom_prim_path, stage=stage) # Update references to exported Xform and make it instanceable geom_undef_prim = stage.DefinePrim(geom_prim_path) geom_undef_prim.GetReferences().AddReference(self.usd_instanceable_meshes_path, primPath=geom_prim_path) @@ -217,7 +220,6 @@ async def _convert_mesh_to_usd(in_file: str, out_file: str, load_materials: bool enable_extension("omni.kit.asset_converter") import omni.kit.asset_converter - import omni.usd # Create converter context converter_context = omni.kit.asset_converter.AssetConverterContext() diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py index 97e66fd46e91..3d231ac1efee 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/converters/mjcf_converter.py b/source/isaaclab/isaaclab/sim/converters/mjcf_converter.py index aa1e52be339d..17808f59b948 100644 --- a/source/isaaclab/isaaclab/sim/converters/mjcf_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mjcf_converter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,14 +6,16 @@ from __future__ import annotations import os +from typing import TYPE_CHECKING -import isaacsim import omni.kit.commands -import omni.usd from .asset_converter_base import AssetConverterBase from .mjcf_converter_cfg import MjcfConverterCfg +if TYPE_CHECKING: + import isaacsim.asset.importer.mjcf + class MjcfConverter(AssetConverterBase): """Converter for a MJCF description file to a USD file. @@ -65,7 +67,7 @@ def _convert_asset(self, cfg: MjcfConverterCfg): prim_path=f"/{file_basename}", ) - def _get_mjcf_import_config(self) -> isaacsim.asset.importer.mjcf.ImportConfig: + def _get_mjcf_import_config(self) -> isaacsim.asset.importer.mjcf._mjcf.ImportConfig: """Returns the import configuration for MJCF to USD conversion. Returns: diff --git a/source/isaaclab/isaaclab/sim/converters/mjcf_converter_cfg.py b/source/isaaclab/isaaclab/sim/converters/mjcf_converter_cfg.py index b1844f645ce5..7cbd83e3e9f2 100644 --- a/source/isaaclab/isaaclab/sim/converters/mjcf_converter_cfg.py +++ b/source/isaaclab/isaaclab/sim/converters/mjcf_converter_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/converters/urdf_converter.py b/source/isaaclab/isaaclab/sim/converters/urdf_converter.py index 82cf55d54055..ba80f520355e 100644 --- a/source/isaaclab/isaaclab/sim/converters/urdf_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/urdf_converter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,16 +7,21 @@ import math import re +from typing import TYPE_CHECKING + +from packaging.version import Version -import isaacsim import omni.kit.app import omni.kit.commands -import omni.usd -from isaacsim.core.utils.extensions import enable_extension + +from isaaclab.utils.version import get_isaac_sim_version from .asset_converter_base import AssetConverterBase from .urdf_converter_cfg import UrdfConverterCfg +if TYPE_CHECKING: + import isaacsim.asset.importer.urdf + class UrdfConverter(AssetConverterBase): """Converter for a URDF description file to a USD file. @@ -32,7 +37,17 @@ class UrdfConverter(AssetConverterBase): .. note:: From Isaac Sim 4.5 onwards, the extension name changed from ``omni.importer.urdf`` to - ``isaacsim.asset.importer.urdf``. This converter class now uses the latest extension from Isaac Sim. + ``isaacsim.asset.importer.urdf``. + + .. note:: + In Isaac Sim 5.1, the URDF importer changed the default behavior of merging fixed joints. + Links connected through ``fixed_joint`` elements are no longer merged when their URDF link + entries specify mass and inertia, even if ``merge-joint`` is set to True. The new behavior + treats links with mass/inertia as full bodies rather than zero-mass reference frames. + + To maintain backwards compatibility, **this converter pins to an older version of the + URDF importer extension** (version 2.4.31) that still merges fixed joints by default. + This allows existing URDFs to work as expected without modification. .. _isaacsim.asset.importer.urdf: https://docs.isaacsim.omniverse.nvidia.com/latest/importer_exporter/ext_isaacsim_asset_importer_urdf.html """ @@ -46,9 +61,13 @@ def __init__(self, cfg: UrdfConverterCfg): Args: cfg: The configuration instance for URDF to USD conversion. """ - manager = omni.kit.app.get_app().get_extension_manager() - if not manager.is_extension_enabled("isaacsim.asset.importer.urdf"): - enable_extension("isaacsim.asset.importer.urdf") + # switch to older version of the URDF importer extension + if get_isaac_sim_version() >= Version("5.1"): + manager = omni.kit.app.get_app().get_extension_manager() + if not manager.is_extension_enabled("isaacsim.asset.importer.urdf-2.4.31"): + manager.set_extension_enabled_immediate("isaacsim.asset.importer.urdf-2.4.31", True) + + # acquire the URDF interface from isaacsim.asset.importer.urdf._urdf import acquire_urdf_interface self._urdf_interface = acquire_urdf_interface() @@ -95,7 +114,7 @@ def _convert_asset(self, cfg: UrdfConverterCfg): Helper methods. """ - def _get_urdf_import_config(self) -> isaacsim.asset.importer.urdf.ImportConfig: + def _get_urdf_import_config(self) -> isaacsim.asset.importer.urdf._urdf.ImportConfig: """Create and fill URDF ImportConfig with desired settings Returns: @@ -121,6 +140,7 @@ def _get_urdf_import_config(self) -> isaacsim.asset.importer.urdf.ImportConfig: import_config.set_collision_from_visuals(self.cfg.collision_from_visuals) # consolidating links that are connected by fixed joints import_config.set_merge_fixed_joints(self.cfg.merge_fixed_joints) + import_config.set_merge_fixed_ignore_inertia(self.cfg.merge_fixed_joints) # -- physics settings # create fix joint for base link import_config.set_fix_base(self.cfg.fix_base) diff --git a/source/isaaclab/isaaclab/sim/converters/urdf_converter_cfg.py b/source/isaaclab/isaaclab/sim/converters/urdf_converter_cfg.py index c6aa6bf5222c..c04ede2400ae 100644 --- a/source/isaaclab/isaaclab/sim/converters/urdf_converter_cfg.py +++ b/source/isaaclab/isaaclab/sim/converters/urdf_converter_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -97,7 +97,9 @@ class NaturalFrequencyGainsCfg: """ link_density: float = 0.0 - """Default density in ``kg/m^3`` for links whose ``"inertial"`` properties are missing in the URDF. Defaults to 0.0.""" + """Default density in ``kg/m^3`` for links whose ``"inertial"`` properties are missing in the URDF. + Defaults to 0.0. + """ merge_fixed_joints: bool = True """Consolidate links that are connected by fixed joints. Defaults to True.""" diff --git a/source/isaaclab/isaaclab/sim/schemas/__init__.py b/source/isaaclab/isaaclab/sim/schemas/__init__.py index d8d04dfc478a..c8402fdb13c8 100644 --- a/source/isaaclab/isaaclab/sim/schemas/__init__.py +++ b/source/isaaclab/isaaclab/sim/schemas/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -33,6 +33,7 @@ """ from .schemas import ( + MESH_APPROXIMATION_TOKENS, PHYSX_MESH_COLLISION_CFGS, USD_MESH_COLLISION_CFGS, activate_contact_sensors, @@ -122,4 +123,5 @@ # Constants for configs that use PhysX vs USD API "PHYSX_MESH_COLLISION_CFGS", "USD_MESH_COLLISION_CFGS", + "MESH_APPROXIMATION_TOKENS", ] diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index 482b6745842b..91eb3b2cd6a9 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,14 +6,17 @@ # needed to import for allowing type-hinting: Usd.Stage | None from __future__ import annotations +import logging import math +from collections.abc import Callable +from typing import Any -import omni.log import omni.physx.scripts.utils as physx_utils -from isaacsim.core.utils.stage import get_current_stage from omni.physx.scripts import deformableUtils as deformable_utils from pxr import PhysxSchema, Usd, UsdPhysics +from isaaclab.sim.utils.stage import get_current_stage + from ..utils import ( apply_nested, find_global_fixed_joint_prim, @@ -22,10 +25,29 @@ ) from . import schemas_cfg +# import logger +logger = logging.getLogger(__name__) + + """ -Articulation root properties. +Constants. """ +# Mapping from string names to USD/PhysX tokens for mesh collision approximation +# Refer to omniverse documentation +# https://docs.omniverse.nvidia.com/kit/docs/omni_physics/latest/dev_guide/rigid_bodies_articulations/collision.html#mesh-geometry-colliders +# for available tokens. +MESH_APPROXIMATION_TOKENS = { + "boundingCube": UsdPhysics.Tokens.boundingCube, + "boundingSphere": UsdPhysics.Tokens.boundingSphere, + "convexDecomposition": UsdPhysics.Tokens.convexDecomposition, + "convexHull": UsdPhysics.Tokens.convexHull, + "none": UsdPhysics.Tokens.none, + "meshSimplification": UsdPhysics.Tokens.meshSimplification, + "sdf": PhysxSchema.Tokens.sdf, +} + + PHYSX_MESH_COLLISION_CFGS = [ schemas_cfg.ConvexDecompositionPropertiesCfg, schemas_cfg.ConvexHullPropertiesCfg, @@ -43,6 +65,11 @@ ] +""" +Articulation root properties. +""" + + def define_articulation_root_properties( prim_path: str, cfg: schemas_cfg.ArticulationRootPropertiesCfg, stage: Usd.Stage | None = None ): @@ -151,12 +178,12 @@ def modify_articulation_root_properties( # if we found a fixed joint, enable/disable it based on the input # otherwise, create a fixed joint between the world and the root link if existing_fixed_joint_prim is not None: - omni.log.info( + logger.info( f"Found an existing fixed joint for the articulation: '{prim_path}'. Setting it to: {fix_root_link}." ) existing_fixed_joint_prim.GetJointEnabledAttr().Set(fix_root_link) elif fix_root_link: - omni.log.info(f"Creating a fixed joint for the articulation: '{prim_path}'.") + logger.info(f"Creating a fixed joint for the articulation: '{prim_path}'.") # note: we have to assume that the root prim is a rigid body, # i.e. we don't handle the case where the root prim is not a rigid body but has articulation api on it @@ -527,10 +554,10 @@ def activate_contact_sensors(prim_path: str, threshold: float = 0.0, stage: Usd. rb.CreateSleepThresholdAttr().Set(0.0) # add contact report API with threshold of zero if not child_prim.HasAPI(PhysxSchema.PhysxContactReportAPI): - omni.log.verbose(f"Adding contact report API to prim: '{child_prim.GetPrimPath()}'") + logger.debug(f"Adding contact report API to prim: '{child_prim.GetPrimPath()}'") cr_api = PhysxSchema.PhysxContactReportAPI.Apply(child_prim) else: - omni.log.verbose(f"Contact report API already exists on prim: '{child_prim.GetPrimPath()}'") + logger.debug(f"Contact report API already exists on prim: '{child_prim.GetPrimPath()}'") cr_api = PhysxSchema.PhysxContactReportAPI.Get(stage, child_prim.GetPrimPath()) # set threshold to zero cr_api.CreateThresholdAttr().Set(threshold) @@ -957,13 +984,26 @@ def modify_deformable_body_properties( """ -def extract_mesh_collision_api_and_attrs(cfg): +def extract_mesh_collision_api_and_attrs( + cfg: schemas_cfg.MeshCollisionPropertiesCfg, +) -> tuple[Callable, dict[str, Any]]: + """Extract the mesh collision API function and custom attributes from the configuration. + + Args: + cfg: The configuration for the mesh collision properties. + + Returns: + A tuple containing the API function to use and a dictionary of custom attributes. + + Raises: + ValueError: When neither USD nor PhysX API can be determined to be used. + """ # We use the number of user set attributes outside of the API function # to determine which API to use in ambiguous cases, so collect them here custom_attrs = { key: value for key, value in cfg.to_dict().items() - if value is not None and key not in ["usd_func", "physx_func"] + if value is not None and key not in ["usd_func", "physx_func", "mesh_approximation_name"] } use_usd_api = False @@ -981,20 +1021,17 @@ def extract_mesh_collision_api_and_attrs(cfg): # Use the PhysX API use_phsyx_api = True - elif len(custom_attrs > 0) and type(cfg) in USD_MESH_COLLISION_CFGS: + elif len(custom_attrs) > 0 and type(cfg) in USD_MESH_COLLISION_CFGS: raise ValueError("Args are specified but the USD Mesh API doesn't support them!") - mesh_collision_appx_type = type(cfg).__name__.partition("PropertiesCfg")[0] - if use_usd_api: - # Add approximation to the attributes as this is how USD collision mesh API is configured + # Use USD API for corresponding attributes + # For mesh collision approximation attribute, we set it explicitly in `modify_mesh_collision_properties`` api_func = cfg.usd_func - # Approximation needs to be formatted with camelCase - custom_attrs["Approximation"] = mesh_collision_appx_type[0].lower() + mesh_collision_appx_type[1:] elif use_phsyx_api: api_func = cfg.physx_func else: - raise ValueError("Either USD or PhysX API should be used for mesh collision approximation!") + raise ValueError("Either USD or PhysX API should be used for modifying mesh collision attributes!") return api_func, custom_attrs @@ -1033,7 +1070,7 @@ def define_mesh_collision_properties( @apply_nested def modify_mesh_collision_properties( prim_path: str, cfg: schemas_cfg.MeshCollisionPropertiesCfg, stage: Usd.Stage | None = None -): +) -> bool: """Set properties for the mesh collision of a prim. These properties are based on either the `Phsyx the `UsdPhysics.MeshCollisionAPI` schema. .. note:: @@ -1045,6 +1082,10 @@ def modify_mesh_collision_properties( cfg : The configuration for the mesh collision properties. stage : The stage where to find the prim. Defaults to None, in which case the current stage is used. + Returns: + True if the properties were successfully set, False otherwise. + Raises: + ValueError: When the mesh approximation name is invalid. """ # obtain stage if stage is None: @@ -1052,6 +1093,21 @@ def modify_mesh_collision_properties( # get USD prim prim = stage.GetPrimAtPath(prim_path) + # we need MeshCollisionAPI to set mesh collision approximation attribute + if not UsdPhysics.MeshCollisionAPI(prim): + UsdPhysics.MeshCollisionAPI.Apply(prim) + # convert mesh approximation string to token + approximation_name = cfg.mesh_approximation_name + if approximation_name not in MESH_APPROXIMATION_TOKENS: + raise ValueError( + f"Invalid mesh approximation name: '{approximation_name}'. " + f"Valid options are: {list(MESH_APPROXIMATION_TOKENS.keys())}" + ) + approximation_token = MESH_APPROXIMATION_TOKENS[approximation_name] + safe_set_attribute_on_usd_schema( + UsdPhysics.MeshCollisionAPI(prim), "Approximation", approximation_token, camel_case=False + ) + api_func, custom_attrs = extract_mesh_collision_api_and_attrs(cfg=cfg) # retrieve the mesh collision API diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py index a131f739e223..446d7faa105d 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -442,8 +442,23 @@ class MeshCollisionPropertiesCfg: """ usd_func: callable = MISSING + """USD API function for modifying mesh collision properties. + Refer to + `original USD Documentation `_ + for more information. + """ physx_func: callable = MISSING + """PhysX API function for modifying mesh collision properties. + Refer to + `original PhysX Documentation `_ + for more information. + """ + + mesh_approximation_name: str = "none" + """Name of mesh collision approximation method. Default: "none". + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ @configclass @@ -453,6 +468,11 @@ class BoundingCubePropertiesCfg(MeshCollisionPropertiesCfg): https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_usd_physics_mesh_collision_a_p_i.html """ + mesh_approximation_name: str = "boundingCube" + """Name of mesh collision approximation method. Default: "boundingCube". + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + @configclass class BoundingSpherePropertiesCfg(MeshCollisionPropertiesCfg): @@ -461,6 +481,11 @@ class BoundingSpherePropertiesCfg(MeshCollisionPropertiesCfg): https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_usd_physics_mesh_collision_a_p_i.html """ + mesh_approximation_name: str = "boundingSphere" + """Name of mesh collision approximation method. Default: "boundingSphere". + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + @configclass class ConvexDecompositionPropertiesCfg(MeshCollisionPropertiesCfg): @@ -474,6 +499,11 @@ class ConvexDecompositionPropertiesCfg(MeshCollisionPropertiesCfg): https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_convex_decomposition_collision_a_p_i.html """ + mesh_approximation_name: str = "convexDecomposition" + """Name of mesh collision approximation method. Default: "convexDecomposition". + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + hull_vertex_limit: int | None = None """Convex hull vertex limit used for convex hull cooking. @@ -518,6 +548,11 @@ class ConvexHullPropertiesCfg(MeshCollisionPropertiesCfg): https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_convex_hull_collision_a_p_i.html """ + mesh_approximation_name: str = "convexHull" + """Name of mesh collision approximation method. Default: "convexHull". + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + hull_vertex_limit: int | None = None """Convex hull vertex limit used for convex hull cooking. @@ -539,6 +574,11 @@ class TriangleMeshPropertiesCfg(MeshCollisionPropertiesCfg): https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_triangle_mesh_collision_a_p_i.html """ + mesh_approximation_name: str = "none" + """Name of mesh collision approximation method. Default: "none" (uses triangle mesh). + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + weld_tolerance: float | None = None """Mesh weld tolerance, controls the distance at which vertices are welded. @@ -559,6 +599,11 @@ class TriangleMeshSimplificationPropertiesCfg(MeshCollisionPropertiesCfg): https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_triangle_mesh_simplification_collision_a_p_i.html """ + mesh_approximation_name: str = "meshSimplification" + """Name of mesh collision approximation method. Default: "meshSimplification". + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + simplification_metric: float | None = None """Mesh simplification accuracy. @@ -583,6 +628,12 @@ class SDFMeshPropertiesCfg(MeshCollisionPropertiesCfg): More details and steps for optimizing SDF results can be found here: https://nvidia-omniverse.github.io/PhysX/physx/5.2.1/docs/RigidBodyCollision.html#dynamic-triangle-meshes-with-sdfs """ + + mesh_approximation_name: str = "sdf" + """Name of mesh collision approximation method. Default: "sdf". + Refer to :const:`schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + sdf_margin: float | None = None """Margin to increase the size of the SDF relative to the bounding box diagonal length of the mesh. @@ -605,7 +656,8 @@ class SDFMeshPropertiesCfg(MeshCollisionPropertiesCfg): Range: [0, 1] Units: dimensionless """ sdf_resolution: int | None = None - """The spacing of the uniformly sampled SDF is equal to the largest AABB extent of the mesh, divided by the resolution. + """The spacing of the uniformly sampled SDF is equal to the largest AABB extent of the mesh, + divided by the resolution. Choose the lowest possible resolution that provides acceptable performance; very high resolution results in large memory consumption, and slower cooking and simulation performance. diff --git a/source/isaaclab/isaaclab/sim/simulation_cfg.py b/source/isaaclab/isaaclab/sim/simulation_cfg.py index 380dba26c510..06ed4826af68 100644 --- a/source/isaaclab/isaaclab/sim/simulation_cfg.py +++ b/source/isaaclab/isaaclab/sim/simulation_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -43,6 +43,26 @@ class PhysxCfg: * :obj:`1`: TGS (Temporal Gauss-Seidel) """ + solve_articulation_contact_last: bool = False + """Changes the ordering inside the articulation solver. Default is False. + + PhysX employs a strict ordering for handling constraints in an articulation. The outcome of + each constraint resolution modifies the joint and associated link speeds. However, the default + ordering may not be ideal for gripping scenarios because the solver favours the constraint + types that are resolved last. This is particularly true of stiff constraint systems that are hard + to resolve without resorting to vanishingly small simulation timesteps. + + With dynamic contact resolution being such an important part of gripping, it may make + more sense to solve dynamic contact towards the end of the solver rather than at the + beginning. This parameter modifies the default ordering to enable this change. + + For more information, please check `here `__. + + .. versionadded:: v2.3 + This parameter is only available with Isaac Sim 5.1. + + """ + min_position_iteration_count: int = 1 """Minimum number of solver position iterations (rigid bodies, cloth, particles etc.). Default is 1. @@ -92,13 +112,27 @@ class PhysxCfg: .. note:: - We recommend setting this flag to true only when the simulation step size is large (i.e., less than 30 Hz or more than 0.0333 seconds). + We recommend setting this flag to true only when the simulation step size is large + (i.e., less than 30 Hz or more than 0.0333 seconds). .. warning:: Enabling this flag may lead to incorrect contact forces report from the contact sensor. """ + enable_external_forces_every_iteration: bool = False + """Enable/disable external forces every position iteration in the TGS solver. Default is False. + + When using the TGS solver (:attr:`solver_type` is 1), this flag allows enabling external forces every solver + position iteration. This can help improve the accuracy of velocity updates. Consider enabling this flag if + the velocities generated by the simulation are noisy. Increasing the number of velocity iterations, together + with this flag, can help improve the accuracy of velocity updates. + + .. note:: + + This flag is ignored when using the PGS solver (:attr:`solver_type` is 0). + """ + enable_enhanced_determinism: bool = False """Enable/disable improved determinism at the expense of performance. Defaults to False. @@ -160,26 +194,6 @@ class PhysxCfg: gpu_max_particle_contacts: int = 2**20 """Size of particle contacts stream buffer allocated in pinned host memory. Default is 2 ** 20.""" - solve_articulation_contact_last: bool = False - """Changes the ordering inside the articulation solver. Default is False. - - PhysX employs a strict ordering for handling constraints in an articulation. The outcome of - each constraint resolution modifies the joint and associated link speeds. However, the default - ordering may not be ideal for gripping scenarios because the solver favours the constraint - types that are resolved last. This is particularly true of stiff constraint systems that are hard - to resolve without resorting to vanishingly small simulation timesteps. - - With dynamic contact resolution being such an important part of gripping, it may make - more sense to solve dynamic contact towards the end of the solver rather than at the - beginning. This parameter modifies the default ordering to enable this change. - - For more information, please check `here `__. - - .. versionadded:: v2.3 - This parameter is only available with Isaac Sim 5.1. - - """ - @configclass class RenderCfg: @@ -200,7 +214,8 @@ class RenderCfg: """ enable_translucency: bool | None = None - """Enables translucency for specular transmissive surfaces such as glass at the cost of some performance. Default is False. + """Enables translucency for specular transmissive surfaces such as glass at the cost of some performance. + Default is False. This is set by the variable: ``/rtx/translucency/enabled``. """ @@ -221,10 +236,11 @@ class RenderCfg: """Selects the anti-aliasing mode to use. Defaults to DLSS. - **DLSS**: Boosts performance by using AI to output higher resolution frames from a lower resolution input. - DLSS samples multiple lower resolution images and uses motion data and feedback from prior frames to reconstruct - native quality images. - - **DLAA**: Provides higher image quality with an AI-based anti-aliasing technique. DLAA uses the same Super Resolution - technology developed for DLSS, reconstructing a native resolution image to maximize image quality. + DLSS samples multiple lower resolution images and uses motion data and feedback from prior frames to + reconstruct native quality images. + - **DLAA**: Provides higher image quality with an AI-based anti-aliasing technique. DLAA uses the same + Super Resolution technology developed for DLSS, reconstructing a native resolution image to maximize + image quality. This is set by the variable: ``/rtx/post/dlss/execMode``. """ @@ -315,8 +331,11 @@ class RenderCfg: """A general dictionary for users to supply all carb rendering settings with native names. The keys of the dictionary can be formatted like a carb setting, .kit file setting, or python variable. - For instance, a key value pair can be ``/rtx/translucency/enabled: False`` (carb), ``rtx.translucency.enabled: False`` (.kit), - or ``rtx_translucency_enabled: False`` (python). + For instance, a key value pair can be: + + - ``/rtx/translucency/enabled: False`` (carb) + - ``rtx.translucency.enabled: False`` (.kit) + - ``rtx_translucency_enabled: False`` (python) """ rendering_mode: Literal["performance", "balanced", "quality"] | None = None @@ -410,3 +429,16 @@ class SimulationCfg: Creating the stage in memory can reduce start-up time. """ + + logging_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "WARNING" + """The logging level. Default is "WARNING".""" + + save_logs_to_file: bool = True + """Save logs to a file. Default is True.""" + + log_dir: str | None = None + """The directory to save the logs to. Default is None. + + If :attr:`save_logs_to_file` is True, the logs will be saved to the directory specified by :attr:`log_dir`. + If None, the logs will be saved to the temp directory. + """ diff --git a/source/isaaclab/isaaclab/sim/simulation_context.py b/source/isaaclab/isaaclab/sim/simulation_context.py index 83277635acfa..d022c2d32a75 100644 --- a/source/isaaclab/isaaclab/sim/simulation_context.py +++ b/source/isaaclab/isaaclab/sim/simulation_context.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,12 +6,10 @@ import builtins import enum import glob -import numpy as np +import logging import os import re import time -import toml -import torch import traceback import weakref from collections.abc import Iterator @@ -19,25 +17,30 @@ from datetime import datetime from typing import Any -import carb import flatdict -import isaacsim.core.utils.stage as stage_utils -import omni.log +import numpy as np +import toml +import torch + +import carb import omni.physx import omni.usd from isaacsim.core.api.simulation_context import SimulationContext as _SimulationContext from isaacsim.core.simulation_manager import SimulationManager -from isaacsim.core.utils.carb import get_carb_setting, set_carb_setting from isaacsim.core.utils.viewports import set_camera_view -from isaacsim.core.version import get_version -from pxr import Gf, PhysxSchema, Sdf, Usd, UsdPhysics +from pxr import Gf, PhysxSchema, Sdf, Usd, UsdPhysics, UsdUtils -from isaaclab.sim.utils import create_new_stage_in_memory, use_stage +import isaaclab.sim as sim_utils +from isaaclab.utils.logger import configure_logging +from isaaclab.utils.version import get_isaac_sim_version from .simulation_cfg import SimulationCfg from .spawners import DomeLightCfg, GroundPlaneCfg from .utils import bind_physics_material +# import logger +logger = logging.getLogger(__name__) + class SimulationContext(_SimulationContext): """A class to control simulation-related events such as physics stepping and rendering. @@ -97,8 +100,8 @@ class RenderMode(enum.IntEnum): control what is updated when the simulation is rendered. This is where the render mode comes in. There are four different render modes: - * :attr:`NO_GUI_OR_RENDERING`: The simulation is running without a GUI and off-screen rendering flag is disabled, - so none of the above are updated. + * :attr:`NO_GUI_OR_RENDERING`: The simulation is running without a GUI and off-screen rendering flag + is disabled, so none of the above are updated. * :attr:`NO_RENDERING`: No rendering, where only 1 is updated at a lower rate. * :attr:`PARTIAL_RENDERING`: Partial rendering, where only 1 and 2 are updated. * :attr:`FULL_RENDERING`: Full rendering, where everything (1, 2, 3) is updated. @@ -129,22 +132,30 @@ def __init__(self, cfg: SimulationCfg | None = None): cfg.validate() self.cfg = cfg # check that simulation is running - if stage_utils.get_current_stage() is None: + if sim_utils.get_current_stage() is None: raise RuntimeError("The stage has not been created. Did you run the simulator?") + # setup logger + self.logger = configure_logging( + logging_level=self.cfg.logging_level, + save_logs_to_file=self.cfg.save_logs_to_file, + log_dir=self.cfg.log_dir, + ) + # create stage in memory if requested if self.cfg.create_stage_in_memory: - self._initial_stage = create_new_stage_in_memory() + self._initial_stage = sim_utils.create_new_stage_in_memory() else: self._initial_stage = omni.usd.get_context().get_stage() + # cache stage if it is not already cached + stage_cache = UsdUtils.StageCache.Get() + stage_id = stage_cache.GetId(self._initial_stage).ToLongInt() + if stage_id < 0: + stage_cache.Insert(self._initial_stage) # acquire settings interface self.carb_settings = carb.settings.get_settings() - # read isaac sim version (this includes build tag, release tag etc.) - # note: we do it once here because it reads the VERSION file from disk and is not expected to change. - self._isaacsim_version = get_version() - # apply carb physics settings self._apply_physics_settings() @@ -254,7 +265,7 @@ def __init__(self, cfg: SimulationCfg | None = None): # add warning about enabling stabilization for large step sizes if not self.cfg.physx.enable_stabilization and (self.cfg.dt > 0.0333): - omni.log.warn( + self.logger.warning( "Large simulation step size (> 0.0333 seconds) is not recommended without enabling stabilization." " Consider setting the `enable_stabilization` flag to True in the PhysxCfg, or reducing the" " simulation step size if you run into physics issues." @@ -274,7 +285,7 @@ def __init__(self, cfg: SimulationCfg | None = None): self._physics_device = SimulationManager.get_physics_sim_device() # create a simulation context to control the simulator - if float(".".join(self._isaacsim_version[2])) < 5: + if get_isaac_sim_version().major < 5: # stage arg is not supported before isaac sim 5.0 super().__init__( stage_units_in_meters=1.0, @@ -353,15 +364,26 @@ def is_fabric_enabled(self) -> bool: def get_version(self) -> tuple[int, int, int]: """Returns the version of the simulator. - This is a wrapper around the ``isaacsim.core.version.get_version()`` function. - The returned tuple contains the following information: - * Major version (int): This is the year of the release (e.g. 2022). - * Minor version (int): This is the half-year of the release (e.g. 1 or 2). - * Patch version (int): This is the patch number of the release (e.g. 0). + * Major version: This is the year of the release (e.g. 2022). + * Minor version: This is the half-year of the release (e.g. 1 or 2). + * Patch version: This is the patch number of the release (e.g. 0). + + .. attention:: + This function is deprecated and will be removed in the future. + We recommend using :func:`isaaclab.utils.version.get_isaac_sim_version` + instead of this function. + + Returns: + A tuple containing the major, minor, and patch versions. + + Example: + >>> sim = SimulationContext() + >>> sim.get_version() + (2022, 1, 0) """ - return int(self._isaacsim_version[2]), int(self._isaacsim_version[3]), int(self._isaacsim_version[4]) + return get_isaac_sim_version().major, get_isaac_sim_version().minor, get_isaac_sim_version().micro """ Operations - New utilities. @@ -408,7 +430,7 @@ def set_render_mode(self, mode: RenderMode): """ # check if mode change is possible -- not possible when no GUI is available if not self._has_gui: - omni.log.warn( + self.logger.warning( f"Cannot change render mode when GUI is disabled. Using the default render mode: {self.render_mode}." ) return @@ -450,7 +472,19 @@ def set_setting(self, name: str, value: Any): name: The name of the setting. value: The value of the setting. """ - self._settings.set(name, value) + # Route through typed setters for correctness and consistency for common scalar types. + if isinstance(value, bool): + self.carb_settings.set_bool(name, value) + elif isinstance(value, int): + self.carb_settings.set_int(name, value) + elif isinstance(value, float): + self.carb_settings.set_float(name, value) + elif isinstance(value, str): + self.carb_settings.set_string(name, value) + elif isinstance(value, (list, tuple)): + self.carb_settings.set(name, value) + else: + raise ValueError(f"Unsupported value type for setting '{name}': {type(value)}") def get_setting(self, name: str) -> Any: """Read the simulation setting using the Carbonite SDK. @@ -461,15 +495,7 @@ def get_setting(self, name: str) -> Any: Returns: The value of the setting. """ - return self._settings.get(name) - - def forward(self) -> None: - """Updates articulation kinematics and fabric for rendering.""" - if self._fabric_iface is not None: - if self.physics_sim_view is not None and self.is_playing(): - # Update the articulations' link's poses before rendering - self.physics_sim_view.update_articulations_kinematic() - self._update_fabric(0.0, 0.0) + return self.carb_settings.get(name) def get_initial_stage(self) -> Usd.Stage: """Returns stage handle used during scene creation. @@ -504,6 +530,14 @@ def reset(self, soft: bool = False): self.render() self._disable_app_control_on_stop_handle = False + def forward(self) -> None: + """Updates articulation kinematics and fabric for rendering.""" + if self._fabric_iface is not None: + if self.physics_sim_view is not None and self.is_playing(): + # Update the articulations' link's poses before rendering + self.physics_sim_view.update_articulations_kinematic() + self._update_fabric(0.0, 0.0) + def step(self, render: bool = True): """Steps the simulation. @@ -524,7 +558,7 @@ def step(self, render: bool = True): if self._anim_recording_enabled: is_anim_recording_finished = self._update_anim_recording() if is_anim_recording_finished: - carb.log_warn("[INFO][SimulationContext]: Animation recording finished. Closing app.") + logger.warning("[INFO][SimulationContext]: Animation recording finished. Closing app.") self._app.shutdown() # check if the simulation timeline is paused. in that case keep stepping until it is playing @@ -613,7 +647,7 @@ async def reset_async(self, soft: bool = False): def _init_stage(self, *args, **kwargs) -> Usd.Stage: _ = super()._init_stage(*args, **kwargs) - with use_stage(self.get_initial_stage()): + with sim_utils.use_stage(self.get_initial_stage()): # a stage update here is needed for the case when physics_dt != rendering_dt, otherwise the app crashes # when in headless mode self.set_setting("/app/player/playSimulations", False) @@ -653,31 +687,34 @@ def _apply_physics_settings(self): """Sets various carb physics settings.""" # enable hydra scene-graph instancing # note: this allows rendering of instanceable assets on the GUI - set_carb_setting(self.carb_settings, "/persistent/omnihydra/useSceneGraphInstancing", True) + self.carb_settings.set_bool("/persistent/omnihydra/useSceneGraphInstancing", True) # change dispatcher to use the default dispatcher in PhysX SDK instead of carb tasking # note: dispatcher handles how threads are launched for multi-threaded physics - set_carb_setting(self.carb_settings, "/physics/physxDispatcher", True) + self.carb_settings.set_bool("/physics/physxDispatcher", True) # disable contact processing in omni.physx # note: we disable it by default to avoid the overhead of contact processing when it isn't needed. # The physics flag gets enabled when a contact sensor is created. if hasattr(self.cfg, "disable_contact_processing"): - omni.log.warn( + self.logger.warning( "The `disable_contact_processing` attribute is deprecated and always set to True" " to avoid unnecessary overhead. Contact processing is automatically enabled when" " a contact sensor is created, so manual configuration is no longer required." ) # FIXME: From investigation, it seems this flag only affects CPU physics. For GPU physics, contacts # are always processed. The issue is reported to the PhysX team by @mmittal. - set_carb_setting(self.carb_settings, "/physics/disableContactProcessing", True) + self.carb_settings.set_bool("/physics/disableContactProcessing", True) # disable custom geometry for cylinder and cone collision shapes to allow contact reporting for them # reason: cylinders and cones aren't natively supported by PhysX so we need to use custom geometry flags # reference: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/Geometry.html?highlight=capsule#geometry - set_carb_setting(self.carb_settings, "/physics/collisionConeCustomGeometry", False) - set_carb_setting(self.carb_settings, "/physics/collisionCylinderCustomGeometry", False) + self.carb_settings.set_bool("/physics/collisionConeCustomGeometry", False) + self.carb_settings.set_bool("/physics/collisionCylinderCustomGeometry", False) # hide the Simulation Settings window - set_carb_setting(self.carb_settings, "/physics/autoPopupSimulationOutputWindow", False) + self.carb_settings.set_bool("/physics/autoPopupSimulationOutputWindow", False) + self.carb_settings.set_bool("/physics/visualizationSimulationOutput", False) + # set fabric enabled flag + self.carb_settings.set_bool("/physics/fabricEnabled", self.cfg.use_fabric) - def _apply_render_settings_from_cfg(self): + def _apply_render_settings_from_cfg(self): # noqa: C901 """Sets rtx settings specified in the RenderCfg.""" # define mapping of user-friendly RenderCfg names to native carb names @@ -701,7 +738,7 @@ def _apply_render_settings_from_cfg(self): # 1. command line argument --rendering_mode, if provided # 2. rendering_mode from Render Config, if set # 3. lastly, default to "balanced" mode, if neither is specified - rendering_mode = get_carb_setting(self.carb_settings, "/isaaclab/rendering/rendering_mode") + rendering_mode = self.carb_settings.get("/isaaclab/rendering/rendering_mode") if not rendering_mode: rendering_mode = self.cfg.render.rendering_mode if not rendering_mode: @@ -719,7 +756,7 @@ def _apply_render_settings_from_cfg(self): # grab isaac lab apps path isaaclab_app_exp_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), *[".."] * 4, "apps") # for Isaac Sim 4.5 compatibility, we use the 4.5 rendering mode app files in a different folder - if float(".".join(self._isaacsim_version[2])) < 5: + if get_isaac_sim_version().major < 5: isaaclab_app_exp_path = os.path.join(isaaclab_app_exp_path, "isaacsim_4_5") # grab preset settings @@ -731,7 +768,7 @@ def _apply_render_settings_from_cfg(self): # set presets for key, value in preset_dict.items(): key = "/" + key.replace(".", "/") # convert to carb setting format - set_carb_setting(self.carb_settings, key, value) + self.set_setting(key, value) # set user-friendly named settings for key, value in vars(self.cfg.render).items(): @@ -744,7 +781,7 @@ def _apply_render_settings_from_cfg(self): " need to be updated." ) key = rendering_setting_name_mapping[key] - set_carb_setting(self.carb_settings, key, value) + self.set_setting(key, value) # set general carb settings carb_settings = self.cfg.render.carb_settings @@ -754,9 +791,9 @@ def _apply_render_settings_from_cfg(self): key = "/" + key.replace("_", "/") # convert from python variable style string elif "." in key: key = "/" + key.replace(".", "/") # convert from .kit file style string - if get_carb_setting(self.carb_settings, key) is None: + if self.get_setting(key) is None: raise ValueError(f"'{key}' in RenderCfg.general_parameters does not map to a carb setting.") - set_carb_setting(self.carb_settings, key, value) + self.set_setting(key, value) # set denoiser mode if self.cfg.render.antialiasing_mode is not None: @@ -768,8 +805,8 @@ def _apply_render_settings_from_cfg(self): pass # WAR: Ensure /rtx/renderMode RaytracedLighting is correctly cased. - if get_carb_setting(self.carb_settings, "/rtx/rendermode").lower() == "raytracedlighting": - set_carb_setting(self.carb_settings, "/rtx/rendermode", "RaytracedLighting") + if self.carb_settings.get("/rtx/rendermode").lower() == "raytracedlighting": + self.carb_settings.set_string("/rtx/rendermode", "RaytracedLighting") def _set_additional_physx_params(self): """Sets additional PhysX parameters that are not directly supported by the parent class.""" @@ -792,6 +829,19 @@ def _set_additional_physx_params(self): physx_prim.CreateAttribute("physxScene:solveArticulationContactLast", Sdf.ValueTypeNames.Bool).Set( self.cfg.physx.solve_articulation_contact_last ) + # -- Enable external forces every iteration, helps improve the accuracy of velocity updates. + + if self.cfg.physx.solver_type == 1: + if not self.cfg.physx.enable_external_forces_every_iteration: + logger.warning( + "The `enable_external_forces_every_iteration` parameter in the PhysxCfg is set to False. If you are" + " experiencing noisy velocities, consider enabling this flag. You may need to slightly increase the" + " number of velocity iterations (setting it to 1 or 2 rather than 0), together with this flag, to" + " improve the accuracy of velocity updates." + ) + physx_scene_api.CreateEnableExternalForcesEveryIterationAttr( + self.cfg.physx.enable_external_forces_every_iteration + ) # -- Gravity # note: Isaac sim only takes the "up-axis" as the gravity direction. But physics allows any direction so we @@ -881,10 +931,10 @@ def _setup_anim_recording(self): self._physxPvdInterface = _physxPvd.acquire_physx_pvd_interface() # Set carb settings for the output path and enabling pvd recording - set_carb_setting( - self.carb_settings, "/persistent/physics/omniPvdOvdRecordingDirectory", self._anim_recording_output_dir + self.carb_settings.set_string( + "/persistent/physics/omniPvdOvdRecordingDirectory", self._anim_recording_output_dir ) - set_carb_setting(self.carb_settings, "/physics/omniPvdOutputEnabled", True) + self.carb_settings.set_bool("/physics/omniPvdOutputEnabled", True) def _update_usda_start_time(self, file_path, start_time): """Updates the start time of the USDA baked anim recordingfile.""" @@ -912,7 +962,7 @@ def _update_usda_start_time(self, file_path, start_time): def _finish_anim_recording(self): """Finishes the animation recording and outputs the baked animation recording.""" - carb.log_warn( + logger.warning( "[INFO][SimulationContext]: Finishing animation recording. Stage must be saved. Might take a few minutes." ) @@ -922,7 +972,7 @@ def _finish_anim_recording(self): # Save stage to disk stage_path = os.path.join(self._anim_recording_output_dir, "stage_simulation.usdc") - stage_utils.save_stage(stage_path, save_and_reload_in_place=False) + sim_utils.save_stage(stage_path, save_and_reload_in_place=False) # Find the latest ovd file not named tmp.ovd ovd_files = [ @@ -949,7 +999,7 @@ def _finish_anim_recording(self): ) # Disable recording - set_carb_setting(self.carb_settings, "/physics/omniPvdOutputEnabled", False) + self.carb_settings.set_bool("/physics/omniPvdOutputEnabled", False) return result @@ -997,20 +1047,20 @@ def build_simulation_context( aspects of the simulation, such as time step, gravity, device, and scene elements like ground plane and lighting. - If :attr:`sim_cfg` is None, then an instance of :class:`SimulationCfg` is created with default settings, with parameters - overwritten based on arguments to the function. + If :attr:`sim_cfg` is None, then an instance of :class:`SimulationCfg` is created with default settings, + with parameters overwritten based on arguments to the function. An example usage of the context manager function: .. code-block:: python with build_simulation_context() as sim: - # Design the scene + # Design the scene - # Play the simulation - sim.reset() - while sim.is_playing(): - sim.step() + # Play the simulation + sim.reset() + while sim.is_playing(): + sim.step() Args: create_new_stage: Whether to create a new stage. Defaults to True. @@ -1029,7 +1079,7 @@ def build_simulation_context( """ try: if create_new_stage: - stage_utils.create_new_stage() + sim_utils.create_new_stage() if sim_cfg is None: # Construct one and overwrite the dt, gravity, and device @@ -1066,7 +1116,7 @@ def build_simulation_context( yield sim except Exception: - omni.log.error(traceback.format_exc()) + sim.logger.error(traceback.format_exc()) raise finally: if not sim.has_gui(): diff --git a/source/isaaclab/isaaclab/sim/spawners/__init__.py b/source/isaaclab/isaaclab/sim/spawners/__init__.py index 75484f6a7f28..916141906e1e 100644 --- a/source/isaaclab/isaaclab/sim/spawners/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/from_files/__init__.py b/source/isaaclab/isaaclab/sim/spawners/from_files/__init__.py index 0bfda4d270c0..a95ac491b0a8 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,5 +13,11 @@ """ -from .from_files import spawn_from_urdf, spawn_from_usd, spawn_ground_plane -from .from_files_cfg import GroundPlaneCfg, UrdfFileCfg, UsdFileCfg +from .from_files import ( + spawn_from_mjcf, + spawn_from_urdf, + spawn_from_usd, + spawn_from_usd_with_compliant_contact_material, + spawn_ground_plane, +) +from .from_files_cfg import GroundPlaneCfg, MjcfFileCfg, UrdfFileCfg, UsdFileCfg, UsdFileWithCompliantContactCfg diff --git a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py index a3c8a44015a2..242829d777e8 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py @@ -1,38 +1,38 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +import logging from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils import omni.kit.commands -import omni.log from pxr import Gf, Sdf, Usd -# from Isaac Sim 4.2 onwards, pxr.Semantics is deprecated -try: - import Semantics -except ModuleNotFoundError: - from pxr import Semantics - -from isaacsim.core.utils.stage import get_current_stage - from isaaclab.sim import converters, schemas +from isaaclab.sim.spawners.materials import RigidBodyMaterialCfg from isaaclab.sim.utils import ( + add_labels, bind_physics_material, bind_visual_material, + change_prim_property, clone, - is_current_stage_in_memory, + create_prim, + get_current_stage, + get_first_matching_child_prim, select_usd_variants, + set_prim_visibility, ) from isaaclab.utils.assets import check_usd_path_with_timeout if TYPE_CHECKING: from . import from_files_cfg +# import logger +logger = logging.getLogger(__name__) + @clone def spawn_from_usd( @@ -120,6 +120,48 @@ def spawn_from_urdf( return _spawn_from_usd_file(prim_path, urdf_loader.usd_path, cfg, translation, orientation) +@clone +def spawn_from_mjcf( + prim_path: str, + cfg: from_files_cfg.MjcfFileCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn an asset from a MJCF file and override the settings with the given config. + + It uses the :class:`MjcfConverter` class to create a USD file from MJCF. This file is then imported + at the specified prim path. + + In case a prim already exists at the given prim path, then the function does not create a new prim + or throw an error that the prim already exists. Instead, it just takes the existing prim and overrides + the settings with the given config. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which + case the translation specified in the generated USD file is used. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case the orientation specified in the generated USD file is used. + + Returns: + The prim of the spawned asset. + + Raises: + FileNotFoundError: If the MJCF file does not exist at the given path. + """ + # mjcf loader to convert mjcf to usd + mjcf_loader = converters.MjcfConverter(cfg) + # spawn asset from the generated usd file + return _spawn_from_usd_file(prim_path, mjcf_loader.usd_path, cfg, translation, orientation) + + def spawn_ground_plane( prim_path: str, cfg: from_files_cfg.GroundPlaneCfg, @@ -152,9 +194,12 @@ def spawn_ground_plane( Raises: ValueError: If the prim path already exists. """ + # Obtain current stage + stage = get_current_stage() + # Spawn Ground-plane - if not prim_utils.is_prim_path_valid(prim_path): - prim_utils.create_prim(prim_path, usd_path=cfg.usd_path, translation=translation, orientation=orientation) + if not stage.GetPrimAtPath(prim_path).IsValid(): + create_prim(prim_path, usd_path=cfg.usd_path, translation=translation, orientation=orientation, stage=stage) else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") @@ -162,48 +207,42 @@ def spawn_ground_plane( if cfg.physics_material is not None: cfg.physics_material.func(f"{prim_path}/physicsMaterial", cfg.physics_material) # Apply physics material to ground plane - collision_prim_path = prim_utils.get_prim_path( - prim_utils.get_first_matching_child_prim( - prim_path, predicate=lambda x: prim_utils.get_prim_type_name(x) == "Plane" - ) + collision_prim = get_first_matching_child_prim( + prim_path, + predicate=lambda _prim: _prim.GetTypeName() == "Plane", + stage=stage, ) - bind_physics_material(collision_prim_path, f"{prim_path}/physicsMaterial") - + if collision_prim is None: + raise ValueError(f"No collision prim found at path: '{prim_path}'.") + # bind physics material to the collision prim + collision_prim_path = str(collision_prim.GetPath()) + bind_physics_material(collision_prim_path, f"{prim_path}/physicsMaterial", stage=stage) + + # Obtain environment prim + environment_prim = stage.GetPrimAtPath(f"{prim_path}/Environment") # Scale only the mesh # Warning: This is specific to the default grid plane asset. - if prim_utils.is_prim_path_valid(f"{prim_path}/Environment"): + if environment_prim.IsValid(): # compute scale from size scale = (cfg.size[0] / 100.0, cfg.size[1] / 100.0, 1.0) # apply scale to the mesh - prim_utils.set_prim_property(f"{prim_path}/Environment", "xformOp:scale", scale) + environment_prim.GetAttribute("xformOp:scale").Set(scale) # Change the color of the plane # Warning: This is specific to the default grid plane asset. if cfg.color is not None: - # avoiding this step if stage is in memory since the "ChangePropertyCommand" kit command - # is not supported in stage in memory - if is_current_stage_in_memory(): - omni.log.warn( - "Ground plane color modification is not supported while the stage is in memory. Skipping operation." - ) - - else: - prop_path = f"{prim_path}/Looks/theGrid/Shader.inputs:diffuse_tint" - - # change the color - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(prop_path), - value=Gf.Vec3f(*cfg.color), - prev=None, - type_to_create_if_not_exist=Sdf.ValueTypeNames.Color3f, - ) + # change the color + change_prim_property( + prop_path=f"{prim_path}/Looks/theGrid/Shader.inputs:diffuse_tint", + value=Gf.Vec3f(*cfg.color), + stage=stage, + type_to_create_if_not_exist=Sdf.ValueTypeNames.Color3f, + ) # Remove the light from the ground plane # It isn't bright enough and messes up with the user's lighting settings - stage = get_current_stage() omni.kit.commands.execute("ToggleVisibilitySelectedPrims", selected_paths=[f"{prim_path}/SphereLight"], stage=stage) - prim = prim_utils.get_prim_at_path(prim_path) + prim = stage.GetPrimAtPath(prim_path) # Apply semantic tags if hasattr(cfg, "semantic_tags") and cfg.semantic_tags is not None: # note: taken from replicator scripts.utils.utils.py @@ -211,15 +250,11 @@ def spawn_ground_plane( # deal with spaces by replacing them with underscores semantic_type_sanitized = semantic_type.replace(" ", "_") semantic_value_sanitized = semantic_value.replace(" ", "_") - # set the semantic API for the instance - instance_name = f"{semantic_type_sanitized}_{semantic_value_sanitized}" - sem = Semantics.SemanticsAPI.Apply(prim, instance_name) - # create semantic type and data attributes - sem.CreateSemanticTypeAttr().Set(semantic_type) - sem.CreateSemanticDataAttr().Set(semantic_value) + # add labels to the prim + add_labels(prim, labels=[semantic_value_sanitized], instance_name=semantic_type_sanitized) # Apply visibility - prim_utils.set_prim_visibility(prim, cfg.visible) + set_prim_visibility(prim, cfg.visible) # return the prim return prim @@ -271,18 +306,21 @@ def _spawn_from_usd_file( else: raise FileNotFoundError(f"USD file not found at path at: '{usd_path}'.") + # Obtain current stage + stage = get_current_stage() # spawn asset if it doesn't exist. - if not prim_utils.is_prim_path_valid(prim_path): + if not stage.GetPrimAtPath(prim_path).IsValid(): # add prim as reference to stage - prim_utils.create_prim( + create_prim( prim_path, usd_path=usd_path, translation=translation, orientation=orientation, scale=cfg.scale, + stage=stage, ) else: - omni.log.warn(f"A prim already exists at prim path: '{prim_path}'.") + logger.warning(f"A prim already exists at prim path: '{prim_path}'.") # modify variants if hasattr(cfg, "variants") and cfg.variants is not None: @@ -325,7 +363,81 @@ def _spawn_from_usd_file( # create material cfg.visual_material.func(material_path, cfg.visual_material) # apply material - bind_visual_material(prim_path, material_path) + bind_visual_material(prim_path, material_path, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) + + +@clone +def spawn_from_usd_with_compliant_contact_material( + prim_path: str, + cfg: from_files_cfg.UsdFileWithCompliantContactCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, + **kwargs, +) -> Usd.Prim: + """Spawn an asset from a USD file and apply physics material to specified prims. + + This function extends the :meth:`spawn_from_usd` function by allowing application of compliant contact + physics materials to specified prims within the spawned asset. This is useful for configuring + contact behavior of specific parts within the asset. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance containing the USD file path and physics material settings. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which + case the translation specified in the USD file is used. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case the orientation specified in the USD file is used. + **kwargs: Additional keyword arguments, like ``clone_in_fabric``. + + Returns: + The prim of the spawned asset with the physics material applied to the specified prims. + + Raises: + FileNotFoundError: If the USD file does not exist at the given path. + """ + + prim = _spawn_from_usd_file(prim_path, cfg.usd_path, cfg, translation, orientation) + stiff = cfg.compliant_contact_stiffness + damp = cfg.compliant_contact_damping + if cfg.physics_material_prim_path is None: + logger.warning("No physics material prim path specified. Skipping physics material application.") + return prim + + if isinstance(cfg.physics_material_prim_path, str): + prim_paths = [cfg.physics_material_prim_path] + else: + prim_paths = cfg.physics_material_prim_path + + if stiff is not None or damp is not None: + material_kwargs = {} + if stiff is not None: + material_kwargs["compliant_contact_stiffness"] = stiff + if damp is not None: + material_kwargs["compliant_contact_damping"] = damp + material_cfg = RigidBodyMaterialCfg(**material_kwargs) + + for path in prim_paths: + if not path.startswith("/"): + rigid_body_prim_path = f"{prim_path}/{path}" + else: + rigid_body_prim_path = path + + material_path = f"{rigid_body_prim_path}/compliant_material" + + # spawn physics material + material_cfg.func(material_path, material_cfg) + + bind_physics_material( + rigid_body_prim_path, + material_path, + ) + logger.info( + f"Applied physics material to prim: {rigid_body_prim_path} with compliance stiffness: {stiff} and" + f" compliance damping: {damp}." + ) + + return prim diff --git a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files_cfg.py b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files_cfg.py index f2914fa50438..ad9f55859040 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -39,10 +39,10 @@ class FileCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg): articulation_props: schemas.ArticulationRootPropertiesCfg | None = None """Properties to apply to the articulation root.""" - fixed_tendons_props: schemas.FixedTendonsPropertiesCfg | None = None + fixed_tendons_props: schemas.FixedTendonPropertiesCfg | None = None """Properties to apply to the fixed tendons (if any).""" - spatial_tendons_props: schemas.SpatialTendonsPropertiesCfg | None = None + spatial_tendons_props: schemas.SpatialTendonPropertiesCfg | None = None """Properties to apply to the spatial tendons (if any).""" joint_drive_props: schemas.JointDrivePropertiesCfg | None = None @@ -104,9 +104,9 @@ class UsdFileCfg(FileCfg): variants: object | dict[str, str] | None = None """Variants to select from in the input USD file. Defaults to None, in which case no variants are applied. - This can either be a configclass object, in which case each attribute is used as a variant set name and its specified value, - or a dictionary mapping between the two. Please check the :meth:`~isaaclab.sim.utils.select_usd_variants` function - for more information. + This can either be a configclass object, in which case each attribute is used as a variant set name and + its specified value, or a dictionary mapping between the two. Please check the + :meth:`~isaaclab.sim.utils.select_usd_variants` function for more information. """ @@ -132,11 +132,68 @@ class UrdfFileCfg(FileCfg, converters.UrdfConverterCfg): func: Callable = from_files.spawn_from_urdf +@configclass +class MjcfFileCfg(FileCfg, converters.MjcfConverterCfg): + """MJCF file to spawn asset from. + + It uses the :class:`MjcfConverter` class to create a USD file from MJCF and spawns the imported + USD file. Similar to the :class:`UsdFileCfg`, the generated USD file can be modified by specifying + the respective properties in the configuration class. + + See :meth:`spawn_from_mjcf` for more information. + + .. note:: + The configuration parameters include various properties. If not `None`, these properties + are modified on the spawned prim in a nested manner. + + If they are set to a value, then the properties are modified on the spawned prim in a nested manner. + This is done by calling the respective function with the specified properties. + + """ + + func: Callable = from_files.spawn_from_mjcf + + """ Spawning ground plane. """ +@configclass +class UsdFileWithCompliantContactCfg(UsdFileCfg): + """Configuration for spawning a USD asset with compliant contact physics material. + + This class extends :class:`UsdFileCfg` to support applying compliant contact properties + (stiffness and damping) to specific prims in the spawned asset. It uses the + :meth:`spawn_from_usd_with_compliant_contact_material` function to perform the spawning and + material application. + """ + + func: Callable = from_files.spawn_from_usd_with_compliant_contact_material + + compliant_contact_stiffness: float | None = None + """Stiffness of the compliant contact. Defaults to None. + + This parameter is the same as + :attr:`~isaaclab.sim.spawners.materials.RigidBodyMaterialCfg.compliant_contact_stiffness`. + """ + + compliant_contact_damping: float | None = None + """Damping of the compliant contact. Defaults to None. + + This parameter is the same as + :attr:`isaaclab.sim.spawners.materials.RigidBodyMaterialCfg.compliant_contact_damping`. + """ + + physics_material_prim_path: str | list[str] | None = None + """Path to the prim or prims to apply the physics material to. Defaults to None, in which case the + physics material is not applied. + + If the path is relative, then it will be relative to the prim's path. + If None, then the physics material will not be applied. + """ + + @configclass class GroundPlaneCfg(SpawnerCfg): """Create a ground plane prim. diff --git a/source/isaaclab/isaaclab/sim/spawners/lights/__init__.py b/source/isaaclab/isaaclab/sim/spawners/lights/__init__.py index ecb8d3a167f1..df0c638f58f1 100644 --- a/source/isaaclab/isaaclab/sim/spawners/lights/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/lights/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/lights/lights.py b/source/isaaclab/isaaclab/sim/spawners/lights/lights.py index dccd00f4bca7..9b0106c6ecdd 100644 --- a/source/isaaclab/isaaclab/sim/spawners/lights/lights.py +++ b/source/isaaclab/isaaclab/sim/spawners/lights/lights.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +7,9 @@ from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils from pxr import Usd, UsdLux -from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_prim +from isaaclab.sim.utils import clone, create_prim, get_current_stage, safe_set_attribute_on_usd_prim if TYPE_CHECKING: from . import lights_cfg @@ -45,11 +44,15 @@ def spawn_light( Raises: ValueError: When a prim already exists at the specified prim path. """ + # obtain stage handle + stage = get_current_stage() # check if prim already exists - if prim_utils.is_prim_path_valid(prim_path): + if stage.GetPrimAtPath(prim_path).IsValid(): raise ValueError(f"A prim already exists at path: '{prim_path}'.") # create the prim - prim = prim_utils.create_prim(prim_path, prim_type=cfg.prim_type, translation=translation, orientation=orientation) + prim = create_prim( + prim_path, prim_type=cfg.prim_type, translation=translation, orientation=orientation, stage=stage + ) # convert to dict cfg = cfg.to_dict() diff --git a/source/isaaclab/isaaclab/sim/spawners/lights/lights_cfg.py b/source/isaaclab/isaaclab/sim/spawners/lights/lights_cfg.py index 59330c77be18..60060ea22e52 100644 --- a/source/isaaclab/isaaclab/sim/spawners/lights/lights_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/lights/lights_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -129,10 +129,12 @@ class DomeLightCfg(LightCfg): Valid values are: - * ``"automatic"``: Tries to determine the layout from the file itself. For example, Renderman texture files embed an explicit parameterization. + * ``"automatic"``: Tries to determine the layout from the file itself. For example, Renderman texture files + embed an explicit parameterization. * ``"latlong"``: Latitude as X, longitude as Y. * ``"mirroredBall"``: An image of the environment reflected in a sphere, using an implicitly orthogonal projection. - * ``"angular"``: Similar to mirroredBall but the radial dimension is mapped linearly to the angle, providing better sampling at the edges. + * ``"angular"``: Similar to mirroredBall but the radial dimension is mapped linearly to the angle, providing better + sampling at the edges. * ``"cubeMapVerticalCross"``: A cube map with faces laid out as a vertical cross. """ diff --git a/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py b/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py index 966efec76b84..0b0e7851494f 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +31,6 @@ Usage: .. code-block:: python - import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils # create a visual material diff --git a/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials.py b/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials.py index e8977a14fd24..29818d830951 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,10 @@ from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils -from isaacsim.core.utils.stage import get_current_stage from pxr import PhysxSchema, Usd, UsdPhysics, UsdShade from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_schema +from isaaclab.sim.utils.stage import get_current_stage if TYPE_CHECKING: from . import physics_materials_cfg @@ -45,11 +44,11 @@ def spawn_rigid_body_material(prim_path: str, cfg: physics_materials_cfg.RigidBo stage = get_current_stage() # create material prim if no prim exists - if not prim_utils.is_prim_path_valid(prim_path): + if not stage.GetPrimAtPath(prim_path).IsValid(): _ = UsdShade.Material.Define(stage, prim_path) # obtain prim - prim = prim_utils.get_prim_at_path(prim_path) + prim = stage.GetPrimAtPath(prim_path) # check if prim is a material if not prim.IsA(UsdShade.Material): raise ValueError(f"A prim already exists at path: '{prim_path}' but is not a material.") @@ -106,11 +105,11 @@ def spawn_deformable_body_material(prim_path: str, cfg: physics_materials_cfg.De stage = get_current_stage() # create material prim if no prim exists - if not prim_utils.is_prim_path_valid(prim_path): + if not stage.GetPrimAtPath(prim_path).IsValid(): _ = UsdShade.Material.Define(stage, prim_path) # obtain prim - prim = prim_utils.get_prim_at_path(prim_path) + prim = stage.GetPrimAtPath(prim_path) # check if prim is a material if not prim.IsA(UsdShade.Material): raise ValueError(f"A prim already exists at path: '{prim_path}' but is not a material.") diff --git a/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py b/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py index 81351305ab7a..ce05c2b9ea46 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py b/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py index 3c79f6f679e5..074d6ac0e432 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials.py @@ -1,23 +1,26 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +import logging from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils -import omni.kit.commands -import omni.log -from pxr import Usd +from omni.usd.commands import CreateMdlMaterialPrimCommand, CreateShaderPrimFromSdrCommand +from pxr import Usd, UsdShade -from isaaclab.sim.utils import attach_stage_to_usd_context, clone, safe_set_attribute_on_usd_prim +from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_prim +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.utils.assets import NVIDIA_NUCLEUS_DIR if TYPE_CHECKING: from . import visual_materials_cfg +# import logger +logger = logging.getLogger(__name__) + @clone def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfaceCfg) -> Usd.Prim: @@ -27,9 +30,9 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa both *specular* and *metallic* workflows. All color inputs are in linear color space (RGB). For more information, see the `documentation `__. - The function calls the USD command `CreatePreviewSurfaceMaterialPrim`_ to create the prim. + The function calls the USD command `CreateShaderPrimFromSdrCommand`_ to create the prim. - .. _CreatePreviewSurfaceMaterialPrim: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.CreatePreviewSurfaceMaterialPrimCommand.html + .. _CreateShaderPrimFromSdrCommand: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.CreateShaderPrimFromSdrCommand.html .. note:: This function is decorated with :func:`clone` that resolves prim path into list of paths @@ -47,20 +50,44 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa Raises: ValueError: If a prim already exists at the given path. """ - # spawn material if it doesn't exist. - if not prim_utils.is_prim_path_valid(prim_path): - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "CreatePreviewSurfaceMaterialPrim" kit command - attach_stage_to_usd_context(attaching_early=True) + # get stage handle + stage = get_current_stage() - omni.kit.commands.execute("CreatePreviewSurfaceMaterialPrim", mtl_path=prim_path, select_new_prim=False) + # spawn material if it doesn't exist. + if not stage.GetPrimAtPath(prim_path).IsValid(): + # note: we don't use Omniverse's CreatePreviewSurfaceMaterialPrimCommand + # since it does not support USD stage as an argument. The created material + # in that case is always the one from USD Context which makes it difficult to + # handle scene creation on a custom stage. + material_prim = UsdShade.Material.Define(stage, prim_path) + if material_prim: + shader_prim = CreateShaderPrimFromSdrCommand( + parent_path=prim_path, + identifier="UsdPreviewSurface", + stage_or_context=stage, + name="Shader", + ).do() + # bind the shader graph to the material + if shader_prim: + surface_out = shader_prim.GetOutput("surface") + if surface_out: + material_prim.CreateSurfaceOutput().ConnectToSource(surface_out) + + displacement_out = shader_prim.GetOutput("displacement") + if displacement_out: + material_prim.CreateDisplacementOutput().ConnectToSource(displacement_out) + else: + raise ValueError(f"Failed to create preview surface shader at path: '{prim_path}'.") else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") # obtain prim - prim = prim_utils.get_prim_at_path(f"{prim_path}/Shader") + prim = stage.GetPrimAtPath(f"{prim_path}/Shader") + # check prim is valid + if not prim.IsValid(): + raise ValueError(f"Failed to create preview surface material at path: '{prim_path}'.") # apply properties - cfg = cfg.to_dict() + cfg = cfg.to_dict() # type: ignore del cfg["func"] for attr_name, attr_value in cfg.items(): safe_set_attribute_on_usd_prim(prim, f"inputs:{attr_name}", attr_value, camel_case=True) @@ -69,7 +96,9 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa @clone -def spawn_from_mdl_file(prim_path: str, cfg: visual_materials_cfg.MdlMaterialCfg) -> Usd.Prim: +def spawn_from_mdl_file( + prim_path: str, cfg: visual_materials_cfg.MdlFileCfg | visual_materials_cfg.GlassMdlCfg +) -> Usd.Prim: """Load a material from its MDL file and override the settings with the given config. NVIDIA's `Material Definition Language (MDL) `__ @@ -97,27 +126,29 @@ def spawn_from_mdl_file(prim_path: str, cfg: visual_materials_cfg.MdlMaterialCfg Raises: ValueError: If a prim already exists at the given path. """ - # spawn material if it doesn't exist. - if not prim_utils.is_prim_path_valid(prim_path): - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "CreateMdlMaterialPrim" kit command - attach_stage_to_usd_context(attaching_early=True) + # get stage handle + stage = get_current_stage() + # spawn material if it doesn't exist. + if not stage.GetPrimAtPath(prim_path).IsValid(): # extract material name from path material_name = cfg.mdl_path.split("/")[-1].split(".")[0] - omni.kit.commands.execute( - "CreateMdlMaterialPrim", + CreateMdlMaterialPrimCommand( mtl_url=cfg.mdl_path.format(NVIDIA_NUCLEUS_DIR=NVIDIA_NUCLEUS_DIR), mtl_name=material_name, mtl_path=prim_path, + stage=stage, select_new_prim=False, - ) + ).do() else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") # obtain prim - prim = prim_utils.get_prim_at_path(f"{prim_path}/Shader") + prim = stage.GetPrimAtPath(f"{prim_path}/Shader") + # check prim is valid + if not prim.IsValid(): + raise ValueError(f"Failed to create MDL material at path: '{prim_path}'.") # apply properties - cfg = cfg.to_dict() + cfg = cfg.to_dict() # type: ignore del cfg["func"] del cfg["mdl_path"] for attr_name, attr_value in cfg.items(): diff --git a/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials_cfg.py b/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials_cfg.py index 569eb6106ed3..a0c2874b0e9a 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/visual_materials_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/meshes/__init__.py b/source/isaaclab/isaaclab/sim/spawners/meshes/__init__.py index f4e66c2aaf63..49836dc5cbd4 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py index 17c23202ed69..066ca0bea188 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py @@ -1,20 +1,20 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np import trimesh import trimesh.transformations -from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils from pxr import Usd, UsdPhysics from isaaclab.sim import schemas -from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone +from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone, create_prim, get_current_stage from ..materials import DeformableBodyMaterialCfg, RigidBodyMaterialCfg @@ -55,10 +55,13 @@ def spawn_mesh_sphere( """ # create a trimesh sphere sphere = trimesh.creation.uv_sphere(radius=cfg.radius) + + # obtain stage handle + stage = get_current_stage() # spawn the sphere as a mesh - _spawn_mesh_geom_from_mesh(prim_path, cfg, sphere, translation, orientation) + _spawn_mesh_geom_from_mesh(prim_path, cfg, sphere, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -91,12 +94,16 @@ def spawn_mesh_cuboid( Raises: ValueError: If a prim already exists at the given path. - """ # create a trimesh box + """ + # create a trimesh box box = trimesh.creation.box(cfg.size) + + # obtain stage handle + stage = get_current_stage() # spawn the cuboid as a mesh - _spawn_mesh_geom_from_mesh(prim_path, cfg, box, translation, orientation, None) + _spawn_mesh_geom_from_mesh(prim_path, cfg, box, translation, orientation, None, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -140,10 +147,13 @@ def spawn_mesh_cylinder( transform = None # create a trimesh cylinder cylinder = trimesh.creation.cylinder(radius=cfg.radius, height=cfg.height, transform=transform) + + # obtain stage handle + stage = get_current_stage() # spawn the cylinder as a mesh - _spawn_mesh_geom_from_mesh(prim_path, cfg, cylinder, translation, orientation) + _spawn_mesh_geom_from_mesh(prim_path, cfg, cylinder, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -187,10 +197,13 @@ def spawn_mesh_capsule( transform = None # create a trimesh capsule capsule = trimesh.creation.capsule(radius=cfg.radius, height=cfg.height, transform=transform) + + # obtain stage handle + stage = get_current_stage() # spawn capsule if it doesn't exist. - _spawn_mesh_geom_from_mesh(prim_path, cfg, capsule, translation, orientation) + _spawn_mesh_geom_from_mesh(prim_path, cfg, capsule, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -234,10 +247,13 @@ def spawn_mesh_cone( transform = None # create a trimesh cone cone = trimesh.creation.cone(radius=cfg.radius, height=cfg.height, transform=transform) + + # obtain stage handle + stage = get_current_stage() # spawn cone if it doesn't exist. - _spawn_mesh_geom_from_mesh(prim_path, cfg, cone, translation, orientation) + _spawn_mesh_geom_from_mesh(prim_path, cfg, cone, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) """ @@ -252,6 +268,7 @@ def _spawn_mesh_geom_from_mesh( translation: tuple[float, float, float] | None = None, orientation: tuple[float, float, float, float] | None = None, scale: tuple[float, float, float] | None = None, + stage: Usd.Stage | None = None, **kwargs, ): """Create a `USDGeomMesh`_ prim from the given mesh. @@ -274,6 +291,7 @@ def _spawn_mesh_geom_from_mesh( orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, in which case this is set to identity. scale: The scale to apply to the prim. Defaults to None, in which case this is set to identity. + stage: The stage to spawn the asset at. Defaults to None, in which case the current stage is used. **kwargs: Additional keyword arguments, like ``clone_in_fabric``. Raises: @@ -285,12 +303,14 @@ def _spawn_mesh_geom_from_mesh( .. _USDGeomMesh: https://openusd.org/dev/api/class_usd_geom_mesh.html """ + # obtain stage handle + stage = stage if stage is not None else get_current_stage() + # spawn geometry if it doesn't exist. - if not prim_utils.is_prim_path_valid(prim_path): - prim_utils.create_prim(prim_path, prim_type="Xform", translation=translation, orientation=orientation) + if not stage.GetPrimAtPath(prim_path).IsValid(): + create_prim(prim_path, prim_type="Xform", translation=translation, orientation=orientation, stage=stage) else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") - # check that invalid schema types are not used if cfg.deformable_props is not None and cfg.rigid_props is not None: raise ValueError("Cannot use both deformable and rigid properties at the same time.") @@ -309,7 +329,7 @@ def _spawn_mesh_geom_from_mesh( mesh_prim_path = geom_prim_path + "/mesh" # create the mesh prim - mesh_prim = prim_utils.create_prim( + mesh_prim = create_prim( mesh_prim_path, prim_type="Mesh", scale=scale, @@ -319,6 +339,7 @@ def _spawn_mesh_geom_from_mesh( "faceVertexCounts": np.asarray([3] * len(mesh.faces)), "subdivisionScheme": "bilinear", }, + stage=stage, ) # note: in case of deformable objects, we need to apply the deformable properties to the mesh prim. @@ -326,9 +347,9 @@ def _spawn_mesh_geom_from_mesh( if cfg.deformable_props is not None: # apply mass properties if cfg.mass_props is not None: - schemas.define_mass_properties(mesh_prim_path, cfg.mass_props) + schemas.define_mass_properties(mesh_prim_path, cfg.mass_props, stage=stage) # apply deformable body properties - schemas.define_deformable_body_properties(mesh_prim_path, cfg.deformable_props) + schemas.define_deformable_body_properties(mesh_prim_path, cfg.deformable_props, stage=stage) elif cfg.collision_props is not None: # decide on type of collision approximation based on the mesh if cfg.__class__.__name__ == "MeshSphereCfg": @@ -343,7 +364,7 @@ def _spawn_mesh_geom_from_mesh( mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(mesh_prim) mesh_collision_api.GetApproximationAttr().Set(collision_approximation) # apply collision properties - schemas.define_collision_properties(mesh_prim_path, cfg.collision_props) + schemas.define_collision_properties(mesh_prim_path, cfg.collision_props, stage=stage) # apply visual material if cfg.visual_material is not None: @@ -354,7 +375,7 @@ def _spawn_mesh_geom_from_mesh( # create material cfg.visual_material.func(material_path, cfg.visual_material) # apply material - bind_visual_material(mesh_prim_path, material_path) + bind_visual_material(mesh_prim_path, material_path, stage=stage) # apply physics material if cfg.physics_material is not None: @@ -365,12 +386,12 @@ def _spawn_mesh_geom_from_mesh( # create material cfg.physics_material.func(material_path, cfg.physics_material) # apply material - bind_physics_material(mesh_prim_path, material_path) + bind_physics_material(mesh_prim_path, material_path, stage=stage) # note: we apply the rigid properties to the parent prim in case of rigid objects. if cfg.rigid_props is not None: # apply mass properties if cfg.mass_props is not None: - schemas.define_mass_properties(prim_path, cfg.mass_props) + schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage) # apply rigid properties - schemas.define_rigid_body_properties(prim_path, cfg.rigid_props) + schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage) diff --git a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes_cfg.py b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes_cfg.py index a6b7f855d040..d5c39a505b8b 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/sensors/__init__.py b/source/isaaclab/isaaclab/sim/spawners/sensors/__init__.py index 9b0e1f6f492c..ac61868c0255 100644 --- a/source/isaaclab/isaaclab/sim/spawners/sensors/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/sensors/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py index 0a385902a55a..6270447169e9 100644 --- a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py +++ b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py @@ -1,23 +1,23 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +import logging from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils -import omni.kit.commands -import omni.log from pxr import Sdf, Usd -from isaaclab.sim.utils import attach_stage_to_usd_context, clone +from isaaclab.sim.utils import change_prim_property, clone, create_prim, get_current_stage from isaaclab.utils import to_camel_case if TYPE_CHECKING: from . import sensors_cfg +# import logger +logger = logging.getLogger(__name__) CUSTOM_PINHOLE_CAMERA_ATTRIBUTES = { "projection_type": ("cameraProjectionType", Sdf.ValueTypeNames.Token), @@ -82,23 +82,21 @@ def spawn_camera( Raises: ValueError: If a prim already exists at the given path. """ + # obtain stage handle + stage = get_current_stage() + # spawn camera if it doesn't exist. - if not prim_utils.is_prim_path_valid(prim_path): - prim_utils.create_prim(prim_path, "Camera", translation=translation, orientation=orientation) + if not stage.GetPrimAtPath(prim_path).IsValid(): + create_prim(prim_path, "Camera", translation=translation, orientation=orientation, stage=stage) else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") # lock camera from viewport (this disables viewport movement for camera) if cfg.lock_camera: - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "ChangePropertyCommand" kit command - attach_stage_to_usd_context(attaching_early=True) - - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(f"{prim_path}.omni:kit:cameraLock"), + change_prim_property( + prop_path=f"{prim_path}.omni:kit:cameraLock", value=True, - prev=None, + stage=stage, type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool, ) # decide the custom attributes to add @@ -110,7 +108,7 @@ def spawn_camera( # TODO: Adjust to handle aperture offsets once supported by omniverse # Internal ticket from rendering team: OM-42611 if cfg.horizontal_aperture_offset > 1e-4 or cfg.vertical_aperture_offset > 1e-4: - omni.log.warn("Camera aperture offsets are not supported by Omniverse. These parameters will be ignored.") + logger.warning("Camera aperture offsets are not supported by Omniverse. These parameters will be ignored.") # custom attributes in the config that are not USD Camera parameters non_usd_cfg_param_names = [ @@ -122,7 +120,7 @@ def spawn_camera( "from_intrinsic_matrix", ] # get camera prim - prim = prim_utils.get_prim_at_path(prim_path) + prim = stage.GetPrimAtPath(prim_path) # create attributes for the fisheye camera model # note: for pinhole those are already part of the USD camera prim for attr_name, attr_type in attribute_types.values(): @@ -145,4 +143,4 @@ def spawn_camera( # get attribute from the class prim.GetAttribute(prim_prop_name).Set(param_value) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return prim diff --git a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py index 189b687f8893..44e5eb061733 100644 --- a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -117,8 +117,8 @@ def from_intrinsic_matrix( 0 & 0 & 1 \\end{bmatrix}, - where :math:`f_x` and :math:`f_y` are the focal length along x and y direction, while :math:`c_x` and :math:`c_y` are the - principle point offsets along x and y direction respectively. + where :math:`f_x` and :math:`f_y` are the focal length along x and y direction, while :math:`c_x` and + :math:`c_y` are the principle point offsets along x and y direction respectively. Args: intrinsic_matrix: Intrinsic matrix of the camera in row-major format. diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/__init__.py b/source/isaaclab/isaaclab/sim/spawners/shapes/__init__.py index 1031103460bc..8f6cab9439cd 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py index f4fa156704af..a7780c25596d 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,10 @@ from typing import TYPE_CHECKING -import isaacsim.core.utils.prims as prim_utils from pxr import Usd from isaaclab.sim import schemas -from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone +from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone, create_prim, get_current_stage if TYPE_CHECKING: from . import shapes_cfg @@ -50,11 +49,13 @@ def spawn_sphere( Raises: ValueError: If a prim already exists at the given path. """ + # obtain stage handle + stage = get_current_stage() # spawn sphere if it doesn't exist. attributes = {"radius": cfg.radius} - _spawn_geom_from_prim_type(prim_path, cfg, "Sphere", attributes, translation, orientation) + _spawn_geom_from_prim_type(prim_path, cfg, "Sphere", attributes, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -94,14 +95,16 @@ def spawn_cuboid( Raises: If a prim already exists at the given path. """ + # obtain stage handle + stage = get_current_stage() # resolve the scale size = min(cfg.size) scale = [dim / size for dim in cfg.size] # spawn cuboid if it doesn't exist. attributes = {"size": size} - _spawn_geom_from_prim_type(prim_path, cfg, "Cube", attributes, translation, orientation, scale) + _spawn_geom_from_prim_type(prim_path, cfg, "Cube", attributes, translation, orientation, scale, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -137,11 +140,13 @@ def spawn_cylinder( Raises: ValueError: If a prim already exists at the given path. """ + # obtain stage handle + stage = get_current_stage() # spawn cylinder if it doesn't exist. attributes = {"radius": cfg.radius, "height": cfg.height, "axis": cfg.axis.upper()} - _spawn_geom_from_prim_type(prim_path, cfg, "Cylinder", attributes, translation, orientation) + _spawn_geom_from_prim_type(prim_path, cfg, "Cylinder", attributes, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -177,11 +182,13 @@ def spawn_capsule( Raises: ValueError: If a prim already exists at the given path. """ + # obtain stage handle + stage = get_current_stage() # spawn capsule if it doesn't exist. attributes = {"radius": cfg.radius, "height": cfg.height, "axis": cfg.axis.upper()} - _spawn_geom_from_prim_type(prim_path, cfg, "Capsule", attributes, translation, orientation) + _spawn_geom_from_prim_type(prim_path, cfg, "Capsule", attributes, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) @clone @@ -217,11 +224,13 @@ def spawn_cone( Raises: ValueError: If a prim already exists at the given path. """ + # obtain stage handle + stage = get_current_stage() # spawn cone if it doesn't exist. attributes = {"radius": cfg.radius, "height": cfg.height, "axis": cfg.axis.upper()} - _spawn_geom_from_prim_type(prim_path, cfg, "Cone", attributes, translation, orientation) + _spawn_geom_from_prim_type(prim_path, cfg, "Cone", attributes, translation, orientation, stage=stage) # return the prim - return prim_utils.get_prim_at_path(prim_path) + return stage.GetPrimAtPath(prim_path) """ @@ -237,6 +246,7 @@ def _spawn_geom_from_prim_type( translation: tuple[float, float, float] | None = None, orientation: tuple[float, float, float, float] | None = None, scale: tuple[float, float, float] | None = None, + stage: Usd.Stage | None = None, ): """Create a USDGeom-based prim with the given attributes. @@ -262,13 +272,17 @@ def _spawn_geom_from_prim_type( orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, in which case this is set to identity. scale: The scale to apply to the prim. Defaults to None, in which case this is set to identity. + stage: The stage to spawn the asset at. Defaults to None, in which case the current stage is used. Raises: ValueError: If a prim already exists at the given path. """ + # obtain stage handle + stage = stage if stage is not None else get_current_stage() + # spawn geometry if it doesn't exist. - if not prim_utils.is_prim_path_valid(prim_path): - prim_utils.create_prim(prim_path, prim_type="Xform", translation=translation, orientation=orientation) + if not stage.GetPrimAtPath(prim_path).IsValid(): + create_prim(prim_path, prim_type="Xform", translation=translation, orientation=orientation, stage=stage) else: raise ValueError(f"A prim already exists at path: '{prim_path}'.") @@ -277,10 +291,10 @@ def _spawn_geom_from_prim_type( mesh_prim_path = geom_prim_path + "/mesh" # create the geometry prim - prim_utils.create_prim(mesh_prim_path, prim_type, scale=scale, attributes=attributes) + create_prim(mesh_prim_path, prim_type, scale=scale, attributes=attributes, stage=stage) # apply collision properties if cfg.collision_props is not None: - schemas.define_collision_properties(mesh_prim_path, cfg.collision_props) + schemas.define_collision_properties(mesh_prim_path, cfg.collision_props, stage=stage) # apply visual material if cfg.visual_material is not None: if not cfg.visual_material_path.startswith("/"): @@ -290,7 +304,7 @@ def _spawn_geom_from_prim_type( # create material cfg.visual_material.func(material_path, cfg.visual_material) # apply material - bind_visual_material(mesh_prim_path, material_path) + bind_visual_material(mesh_prim_path, material_path, stage=stage) # apply physics material if cfg.physics_material is not None: if not cfg.physics_material_path.startswith("/"): @@ -300,12 +314,12 @@ def _spawn_geom_from_prim_type( # create material cfg.physics_material.func(material_path, cfg.physics_material) # apply material - bind_physics_material(mesh_prim_path, material_path) + bind_physics_material(mesh_prim_path, material_path, stage=stage) # note: we apply rigid properties in the end to later make the instanceable prim # apply mass properties if cfg.mass_props is not None: - schemas.define_mass_properties(prim_path, cfg.mass_props) + schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage) # apply rigid body properties if cfg.rigid_props is not None: - schemas.define_rigid_body_properties(prim_path, cfg.rigid_props) + schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage) diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes_cfg.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes_cfg.py index a62024aed69c..d2de5a7f9416 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py b/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py index 3d2dd4d1db0a..2dea8db8fcb6 100644 --- a/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/wrappers/__init__.py b/source/isaaclab/isaaclab/sim/spawners/wrappers/__init__.py index 02c793c34e28..4006fa1a6abc 100644 --- a/source/isaaclab/isaaclab/sim/spawners/wrappers/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/wrappers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py index 9f339aa70c74..64d0c4f4ab91 100644 --- a/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py +++ b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +10,6 @@ from typing import TYPE_CHECKING import carb -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils -from isaacsim.core.utils.stage import get_current_stage from pxr import Sdf, Usd import isaaclab.sim as sim_utils @@ -48,7 +45,7 @@ def spawn_multi_asset( The created prim at the first prim path. """ # get stage handle - stage = get_current_stage() + stage = sim_utils.get_current_stage() # resolve: {SPAWN_NS}/AssetName # note: this assumes that the spawn namespace already exists in the stage @@ -69,8 +66,8 @@ def spawn_multi_asset( source_prim_paths = [root_path] # find a free prim path to hold all the template prims - template_prim_path = stage_utils.get_next_free_path("/World/Template") - prim_utils.create_prim(template_prim_path, "Scope") + template_prim_path = sim_utils.get_next_free_prim_path("/World/Template", stage=stage) + sim_utils.create_prim(template_prim_path, "Scope", stage=stage) # spawn everything first in a "Dataset" prim proto_prim_paths = list() @@ -119,7 +116,7 @@ def spawn_multi_asset( Sdf.CopySpec(env_spec.layer, Sdf.Path(proto_path), env_spec.layer, Sdf.Path(prim_path)) # delete the dataset prim after spawning - prim_utils.delete_prim(template_prim_path) + sim_utils.delete_prim(template_prim_path, stage=stage) # set carb setting to indicate Isaac Lab's environments that different prims have been spawned # at varying prim paths. In this case, PhysX parser shouldn't optimize the stage parsing. @@ -128,7 +125,7 @@ def spawn_multi_asset( carb_settings_iface.set_bool("/isaaclab/spawn/multi_assets", True) # return the prim - return prim_utils.get_prim_at_path(prim_paths[0]) + return stage.GetPrimAtPath(prim_paths[0]) def spawn_multi_usd_file( diff --git a/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers_cfg.py b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers_cfg.py index 1ad1506f2dc6..07c585f7c4c3 100644 --- a/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/sim/utils/__init__.py b/source/isaaclab/isaaclab/sim/utils/__init__.py new file mode 100644 index 000000000000..3a85ae44c2f1 --- /dev/null +++ b/source/isaaclab/isaaclab/sim/utils/__init__.py @@ -0,0 +1,13 @@ +# 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 + +"""Utilities built around USD operations.""" + +from .legacy import * # noqa: F401, F403 +from .prims import * # noqa: F401, F403 +from .queries import * # noqa: F401, F403 +from .semantics import * # noqa: F401, F403 +from .stage import * # noqa: F401, F403 +from .transforms import * # noqa: F401, F403 diff --git a/source/isaaclab/isaaclab/sim/utils/legacy.py b/source/isaaclab/isaaclab/sim/utils/legacy.py new file mode 100644 index 000000000000..0e3aef861733 --- /dev/null +++ b/source/isaaclab/isaaclab/sim/utils/legacy.py @@ -0,0 +1,342 @@ +# 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 + +"""Utilities for legacy functionality. + +This sub-module contains legacy functions from Isaac Sim that are no longer +required for Isaac Lab. Most functions are simple wrappers around USD APIs +and are provided mainly for convenience. + +It is recommended to use the USD APIs directly whenever possible. +""" + +from __future__ import annotations + +import logging +from collections.abc import Iterable + +from pxr import Usd, UsdGeom + +from .prims import add_usd_reference +from .queries import get_next_free_prim_path +from .stage import get_current_stage + +# import logger +logger = logging.getLogger(__name__) + + +""" +Stage utilities. +""" + + +def add_reference_to_stage(usd_path: str, path: str, prim_type: str = "Xform") -> Usd.Prim: + """Adds a USD reference to the stage at the specified prim path. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the :func:`isaaclab.sim.utils.prims.add_usd_reference` function instead. + + Args: + usd_path: The path to the USD file to reference. + path: The prim path to add the reference to. + prim_type: The type of prim to create if it doesn't exist. Defaults to "Xform". + + Returns: + The USD prim at the specified prim path. + """ + logger.warning("Function 'add_reference_to_stage' is deprecated. Please use 'add_usd_reference' instead.") + return add_usd_reference(prim_path=path, usd_path=usd_path, prim_type=prim_type) + + +def get_stage_up_axis() -> str: + """Gets the up axis of the stage. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the USD APIs directly instead. + + >>> import isaaclab.sim as sim_utils + >>> from pxr import UsdGeom + >>> + >>> UsdGeom.GetStageUpAxis(sim_utils.get_current_stage()) + 'Z' + """ + msg = """Function 'get_stage_up_axis' is deprecated. Please use the USD APIs directly instead. + + Example: + >>> import isaaclab.sim as sim_utils + >>> from pxr import UsdGeom + >>> + >>> UsdGeom.GetStageUpAxis(sim_utils.get_current_stage()) + 'Z' + """ + logger.warning(msg) + return UsdGeom.GetStageUpAxis(get_current_stage()) + + +def traverse_stage(fabric: bool = False) -> Iterable[Usd.Prim]: + """Traverses the stage and returns all the prims. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the USD APIs directly instead. + + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> for prim in stage.Traverse(): + >>> print(prim) + Usd.Prim() + Usd.Prim() + Usd.Prim() + Usd.Prim() + + Args: + fabric: True for fabric stage and False for USD stage. Defaults to False. + + Returns: + An iterable of all the prims in the stage. + """ + msg = """Function 'traverse_stage' is deprecated. Please use the USD APIs directly instead. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> for prim in stage.Traverse(): + >>> print(prim) + """ + logger.warning(msg) + # get current stage + stage = get_current_stage(fabric=fabric) + # traverse stage + return stage.Traverse() + + +""" +Prims utilities. +""" + + +def get_prim_at_path(prim_path: str, fabric: bool = False) -> Usd.Prim | None: + """Gets the USD prim at the specified path. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the USD APIs directly instead. + + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> stage.GetPrimAtPath("/World/Cube") + Usd.Prim() + + Args: + prim_path: The path of the prim to get. + fabric: Whether to get the prim from the fabric stage. Defaults to False. + + Returns: + The USD prim at the specified path. If stage is not found, returns None. + """ + msg = """Function 'get_prim_at_path' is deprecated. Please use the USD APIs directly instead. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> stage.GetPrimAtPath("/World/Cube") + Usd.Prim() + """ + logger.warning(msg) + # get current stage + stage = get_current_stage(fabric=fabric) + if stage is not None: + return stage.GetPrimAtPath(prim_path) + return None + + +def get_prim_path(prim: Usd.Prim) -> str: + """Gets the path of the specified USD prim. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the USD APIs directly instead. + + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/Cube") + >>> prim.GetPath().pathString + "/World/Cube" + + Args: + prim: The USD prim to get the path of. + + Returns: + The path of the specified USD prim. + """ + msg = """Function 'get_prim_path' is deprecated. Please use the USD APIs directly instead. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/Cube") + >>> prim.GetPath().pathString + "/World/Cube" + """ + logger.warning(msg) + return prim.GetPath().pathString if prim.IsValid() else "" + + +def is_prim_path_valid(prim_path: str, fabric: bool = False) -> bool: + """Check if a path has a valid USD Prim on the specified stage. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the USD APIs directly instead. + + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/Cube") + >>> prim.IsValid() + True + + Args: + prim_path: path of the prim in the stage + fabric: True for fabric stage and False for USD stage. Defaults to False. + + Returns: + True if the path points to a valid prim. False otherwise. + """ + msg = """Function 'is_prim_path_valid' is deprecated. Please use the USD APIs directly instead. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/Cube") + >>> prim.IsValid() + True + """ + logger.warning(msg) + # get prim at path + prim = get_prim_at_path(prim_path, fabric=fabric) + # return validity + return prim.IsValid() if prim else False + + +def define_prim(prim_path: str, prim_type: str = "Xform", fabric: bool = False) -> Usd.Prim: + """Create a USD Prim at the given prim_path of type prim type unless one already exists. + + This function creates a prim of the specified type in the specified path. To apply a + transformation (position, orientation, scale), set attributes or load an USD file while + creating the prim use the :func:`isaaclab.sim.utils.prims.create_prim` function. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the USD APIs directly instead. + In case, a new prim is needed, use the :func:`isaaclab.sim.utils.prims.create_prim` + function instead. + + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> stage.DefinePrim("/World/Shapes", "Xform") + Usd.Prim() + + Args: + prim_path: path of the prim in the stage + prim_type: The type of the prim to create. Defaults to "Xform". + fabric: True for fabric stage and False for USD stage. Defaults to False. + + Returns: + The created USD prim. + + Raises: + ValueError: If there is already a prim at the prim_path + """ + msg = """Function 'define_prim' is deprecated. Please use the USD APIs directly instead. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> stage.DefinePrim("/World/Shapes", "Xform") + Usd.Prim() + """ + logger.warning(msg) + # get current stage + stage = get_current_stage(fabric=fabric) + # check if prim path is valid + if stage.GetPrimAtPath(prim_path).IsValid(): + raise ValueError(f"A prim already exists at prim path: {prim_path}") + # define prim + return stage.DefinePrim(prim_path, prim_type) + + +def get_prim_type_name(prim_path: str | Usd.Prim, fabric: bool = False) -> str: + """Get the type name of the USD Prim at the provided path. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the USD APIs directly instead. + + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/Cube") + >>> prim.GetTypeName() + "Cube" + + Args: + prim_path: path of the prim in the stage or the prim itself + fabric: True for fabric stage and False for USD stage. Defaults to False. + + Returns: + The type name of the USD Prim at the provided path. + + Raises: + ValueError: If there is not a valid prim at the provided path + """ + msg = """Function 'get_prim_type_name' is deprecated. Please use the USD APIs directly instead. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/Cube") + >>> prim.GetTypeName() + "Cube" + """ + logger.warning(msg) + # check if string + if isinstance(prim_path, str): + stage = get_current_stage(fabric=fabric) + prim = stage.GetPrimAtPath(prim_path) + else: + prim = prim_path + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"A prim does not exist at prim path: {prim_path}") + # return type name + return prim.GetTypeName() + + +""" +Queries utilities. +""" + + +def get_next_free_path(path: str) -> str: + """Gets a new prim path that doesn't exist in the stage given a base path. + + .. deprecated:: 2.3.0 + This function is deprecated. Please use the + :func:`isaaclab.sim.utils.queries.get_next_free_prim_path` function instead. + + Args: + path: The base prim path to check. + stage: The stage to check. Defaults to the current stage. + + Returns: + A new path that is guaranteed to not exist on the current stage + """ + logger.warning("Function 'get_next_free_path' is deprecated. Please use 'get_next_free_prim_path' instead.") + return get_next_free_prim_path(path) diff --git a/source/isaaclab/isaaclab/sim/utils.py b/source/isaaclab/isaaclab/sim/utils/prims.py similarity index 53% rename from source/isaaclab/isaaclab/sim/utils.py rename to source/isaaclab/isaaclab/sim/utils/prims.py index 338ec5d843ad..6fb7850a5b5d 100644 --- a/source/isaaclab/isaaclab/sim/utils.py +++ b/source/isaaclab/isaaclab/sim/utils/prims.py @@ -1,48 +1,342 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 -"""Sub-module with USD-related utilities.""" +"""Utilities for creating and manipulating USD prims.""" from __future__ import annotations -import contextlib import functools import inspect +import logging import re -from collections.abc import Callable, Generator +from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any -import carb -import isaacsim.core.utils.stage as stage_utils -import omni +import torch + import omni.kit.commands -import omni.log +import omni.usd from isaacsim.core.cloner import Cloner -from isaacsim.core.utils.carb import get_carb_setting -from isaacsim.core.utils.stage import get_current_stage -from isaacsim.core.version import get_version from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, UsdShade, UsdUtils -# from Isaac Sim 4.2 onwards, pxr.Semantics is deprecated -try: - import Semantics -except ModuleNotFoundError: - from pxr import Semantics - from isaaclab.utils.string import to_camel_case +from isaaclab.utils.version import get_isaac_sim_version -from . import schemas +from .queries import find_matching_prim_paths +from .semantics import add_labels +from .stage import get_current_stage, get_current_stage_id +from .transforms import convert_world_pose_to_local, standardize_xform_ops if TYPE_CHECKING: - from .spawners.spawner_cfg import SpawnerCfg + from isaaclab.sim.spawners.spawner_cfg import SpawnerCfg + +# import logger +logger = logging.getLogger(__name__) + + +""" +General Utils +""" + + +def create_prim( + prim_path: str, + prim_type: str = "Xform", + position: Any | None = None, + translation: Any | None = None, + orientation: Any | None = None, + scale: Any | None = None, + usd_path: str | None = None, + semantic_label: str | None = None, + semantic_type: str = "class", + attributes: dict | None = None, + stage: Usd.Stage | None = None, +) -> Usd.Prim: + """Creates a prim in the provided USD stage. + + The method applies the specified transforms, the semantic label and sets the specified attributes. + The transform can be specified either in world space (using ``position``) or local space (using + ``translation``). + + The function determines the coordinate system of the transform based on the provided arguments. + + * If ``position`` is provided, it is assumed the orientation is provided in the world frame as well. + * If ``translation`` is provided, it is assumed the orientation is provided in the local frame as well. + + The scale is always applied in the local frame. + + The function handles various sequence types (list, tuple, numpy array, torch tensor) + and converts them to properly-typed tuples for operations on the prim. + + .. note:: + Transform operations are standardized to the USD convention: translate, orient (quaternion), + and scale, in that order. See :func:`standardize_xform_ops` for more details. + + Args: + prim_path: + The path of the new prim. + prim_type: + Prim type name. Defaults to "Xform", in which case a simple Xform prim is created. + position: + Prim position in world space as (x, y, z). If the prim has a parent, this is + automatically converted to local space relative to the parent. Cannot be used with + ``translation``. Defaults to None, in which case no position is applied. + translation: + Prim translation in local space as (x, y, z). This is applied directly without + any coordinate transformation. Cannot be used with ``position``. Defaults to None, + in which case no translation is applied. + orientation: + Prim rotation as a quaternion (w, x, y, z). When used with ``position``, the + orientation is also converted from world space to local space. When used with ``translation``, + it is applied directly as local orientation. Defaults to None. + scale: + Scaling factor in x, y, z. Applied in local space. Defaults to None, + in which case a uniform scale of 1.0 is applied. + usd_path: + Path to the USD file that this prim will reference. Defaults to None. + semantic_label: + Semantic label to apply to the prim. Defaults to None, in which case no label is added. + semantic_type: + Semantic type for the label. Defaults to "class". + attributes: + Key-value pairs of prim attributes to set. Defaults to None, in which case no attributes are set. + stage: + The stage to create the prim in. Defaults to None, in which case the current stage is used. + + Returns: + The created USD prim. + + Raises: + ValueError: If there is already a prim at the provided prim path. + ValueError: If both position and translation are provided. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> # Create a cube at world position (1.0, 0.5, 0.0) + >>> sim_utils.create_prim( + ... prim_path="/World/Parent/Cube", + ... prim_type="Cube", + ... position=(1.0, 0.5, 0.0), + ... attributes={"size": 2.0}, + ... ) + Usd.Prim() + >>> + >>> # Create a sphere with local translation relative to its parent + >>> sim_utils.create_prim( + ... prim_path="/World/Parent/Sphere", + ... prim_type="Sphere", + ... translation=(0.5, 0.0, 0.0), + ... scale=(2.0, 2.0, 2.0), + ... ) + Usd.Prim() + """ + # Ensure that user doesn't provide both position and translation + if position is not None and translation is not None: + raise ValueError("Cannot provide both position and translation. Please provide only one.") + + # obtain stage handle + stage = get_current_stage() if stage is None else stage + + # check if prim already exists + if stage.GetPrimAtPath(prim_path).IsValid(): + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + + # create prim in stage + prim = stage.DefinePrim(prim_path, prim_type) + if not prim.IsValid(): + raise ValueError(f"Failed to create prim at path: '{prim_path}' of type: '{prim_type}'.") + # apply attributes into prim + if attributes is not None: + for k, v in attributes.items(): + prim.GetAttribute(k).Set(v) + # add reference to USD file + if usd_path is not None: + add_usd_reference(prim_path=prim_path, usd_path=usd_path, stage=stage) + # add semantic label to prim + if semantic_label is not None: + add_labels(prim, labels=[semantic_label], instance_name=semantic_type) + + # check if prim type is Xformable + if not prim.IsA(UsdGeom.Xformable): + logger.debug( + f"Prim at path '{prim.GetPath().pathString}' is of type '{prim.GetTypeName()}', " + "which is not an Xformable. Transform operations will not be standardized. " + "This is expected for material, shader, and scope prims." + ) + return prim + + # convert input arguments to tuples + position = _to_tuple(position) if position is not None else None + translation = _to_tuple(translation) if translation is not None else None + orientation = _to_tuple(orientation) if orientation is not None else None + scale = _to_tuple(scale) if scale is not None else None + + # convert position and orientation to translation and orientation + # world --> local + if position is not None: + # this means that user provided pose in the world frame + translation, orientation = convert_world_pose_to_local(position, orientation, ref_prim=prim.GetParent()) + + # standardize the xform ops + standardize_xform_ops(prim, translation, orientation, scale) + + return prim + + +def delete_prim(prim_path: str | Sequence[str], stage: Usd.Stage | None = None) -> bool: + """Removes the USD Prim and its descendants from the scene if able. + + Args: + prim_path: The path of the prim to delete. If a list of paths is provided, + the function will delete all the prims in the list. + stage: The stage to delete the prim in. Defaults to None, in which case the current stage is used. + + Returns: + True if the prim or prims were deleted successfully, False otherwise. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.delete_prim("/World/Cube") + """ + # convert prim_path to list if it is a string + if isinstance(prim_path, str): + prim_path = [prim_path] + # get stage handle + stage = get_current_stage() if stage is None else stage + # FIXME: We should not need to cache the stage here. It should + # happen at the creation of the stage. + # the prim command looks for the stage ID in the stage cache + # so we need to ensure the stage is cached + stage_cache = UsdUtils.StageCache.Get() + stage_id = stage_cache.GetId(stage).ToLongInt() + if stage_id < 0: + stage_id = stage_cache.Insert(stage).ToLongInt() + # delete prims + success, _ = omni.kit.commands.execute( + "DeletePrimsCommand", + paths=prim_path, + stage=stage, + ) + return success + + +def move_prim(path_from: str, path_to: str, keep_world_transform: bool = True, stage: Usd.Stage | None = None) -> bool: + """Moves a prim from one path to another within a USD stage. + + This function moves the prim from the source path to the destination path. If the :attr:`keep_world_transform` + is set to True, the world transform of the prim is kept. This implies that the prim's local transform is reset + such that the prim's world transform is the same as the source path's world transform. If it is set to False, + the prim's local transform is preserved. + + .. warning:: + Reparenting or moving prims in USD is an expensive operation that may trigger + significant recomposition costs, especially in large or deeply layered stages. + + Args: + path_from: Path of the USD Prim you wish to move + path_to: Final destination of the prim + keep_world_transform: Whether to keep the world transform of the prim. Defaults to True. + stage: The stage to move the prim in. Defaults to None, in which case the current stage is used. + + Returns: + True if the prim was moved successfully, False otherwise. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> # given the stage: /World/Cube. Move the prim Cube outside the prim World + >>> sim_utils.move_prim("/World/Cube", "/Cube") + """ + # get stage handle + stage = get_current_stage() if stage is None else stage + # move prim + success, _ = omni.kit.commands.execute( + "MovePrimCommand", + path_from=path_from, + path_to=path_to, + keep_world_transform=keep_world_transform, + stage_or_context=stage, + ) + return success + """ -Attribute - Setters. +USD Prim properties and attributes. """ +def make_uninstanceable(prim_path: str | Sdf.Path, stage: Usd.Stage | None = None): + """Check if a prim and its descendants are instanced and make them uninstanceable. + + This function checks if the prim at the specified prim path and its descendants are instanced. + If so, it makes the respective prim uninstanceable by disabling instancing on the prim. + + This is useful when we want to modify the properties of a prim that is instanced. For example, if we + want to apply a different material on an instanced prim, we need to make the prim uninstanceable first. + + Args: + prim_path: The prim path to check. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # make paths str type if they aren't already + prim_path = str(prim_path) + # check if prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # iterate over all prims under prim-path + all_prims = [prim] + while len(all_prims) > 0: + # get current prim + child_prim = all_prims.pop(0) + # check if prim is instanced + if child_prim.IsInstance(): + # make the prim uninstanceable + child_prim.SetInstanceable(False) + # add children to list + all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) + + +def set_prim_visibility(prim: Usd.Prim, visible: bool) -> None: + """Sets the visibility of the prim in the opened stage. + + .. note:: + + The method does this through the USD API. + + Args: + prim: the USD prim + visible: flag to set the visibility of the usd prim in stage. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> # given the stage: /World/Cube. Make the Cube not visible + >>> prim = sim_utils.get_prim_at_path("/World/Cube") + >>> sim_utils.set_prim_visibility(prim, False) + """ + imageable = UsdGeom.Imageable(prim) + if visible: + imageable.MakeVisible() + else: + imageable.MakeInvisible() + + def safe_set_attribute_on_usd_schema(schema_api: Usd.APISchemaBase, name: str, value: Any, camel_case: bool): """Set the value of an attribute on its USD schema if it exists. @@ -76,7 +370,7 @@ def safe_set_attribute_on_usd_schema(schema_api: Usd.APISchemaBase, name: str, v else: # think: do we ever need to create the attribute if it doesn't exist? # currently, we are not doing this since the schemas are already created with some defaults. - omni.log.error(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.") + logger.error(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.") raise TypeError(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.") @@ -115,23 +409,178 @@ def safe_set_attribute_on_usd_prim(prim: Usd.Prim, attr_name: str, value: Any, c f"Cannot set attribute '{attr_name}' with value '{value}'. Please modify the code to support this type." ) - # early attach stage to usd context if stage is in memory - # since stage in memory is not supported by the "ChangePropertyCommand" kit command - attach_stage_to_usd_context(attaching_early=True) - - # change property - omni.kit.commands.execute( - "ChangePropertyCommand", - prop_path=Sdf.Path(f"{prim.GetPath()}.{attr_name}"), + # change property using the change_prim_property function + change_prim_property( + prop_path=f"{prim.GetPath()}.{attr_name}", value=value, - prev=None, + stage=prim.GetStage(), type_to_create_if_not_exist=sdf_type, - usd_context_name=prim.GetStage(), ) +def change_prim_property( + prop_path: str | Sdf.Path, + value: Any, + stage: Usd.Stage | None = None, + type_to_create_if_not_exist: Sdf.ValueTypeNames | None = None, + is_custom: bool = False, +) -> bool: + """Change or create a property value on a USD prim. + + This is a simplified property setter that works with the current edit target. If you need + complex layer management, use :class:`omni.kit.commands.ChangePropertyCommand` instead. + + By default, this function changes the value of the property when it exists. If the property + doesn't exist, :attr:`type_to_create_if_not_exist` must be provided to create it. + + Note: + The attribute :attr:`value` must be the correct type for the property. + For example, if the property is a float, the value must be a float. + If it is supposed to be a RGB color, the value must be of type :class:`Gf.Vec3f`. + + Args: + prop_path: Property path in the format ``/World/Prim.propertyName``. + value: Value to set. If None, the attribute value goes to its default value. + If the attribute has no default value, it is a silent no-op. + stage: The USD stage. Defaults to None, in which case the current stage is used. + type_to_create_if_not_exist: If not None and property doesn't exist, a new property will + be created with the given type and value. Defaults to None. + is_custom: If the property is created, specify if it is a custom property (not part of + the schema). Defaults to False. + + Returns: + True if the property was successfully changed, False otherwise. + + Raises: + ValueError: If the prim does not exist at the specified path. + + Example: + >>> import isaaclab.sim as sim_utils + >>> from pxr import Sdf + >>> + >>> # Change an existing property + >>> sim_utils.change_prim_property(prop_path="/World/Cube.size", value=2.0) + True + >>> + >>> # Create a new custom property + >>> sim_utils.change_prim_property( + ... prop_path="/World/Cube.customValue", + ... value=42, + ... type_to_create_if_not_exist=Sdf.ValueTypeNames.Int, + ... is_custom=True, + ... ) + True + """ + # get stage handle + stage = get_current_stage() if stage is None else stage + + # convert to Sdf.Path if needed + prop_path = Sdf.Path(prop_path) if isinstance(prop_path, str) else prop_path + + # get the prim path + prim_path = prop_path.GetAbsoluteRootOrPrimPath() + prim = stage.GetPrimAtPath(prim_path) + if not prim or not prim.IsValid(): + raise ValueError(f"Prim does not exist at path: '{prim_path}'") + + # get or create the property + prop = stage.GetPropertyAtPath(prop_path) + + if not prop: + if type_to_create_if_not_exist is not None: + # create new attribute on the prim + prop = prim.CreateAttribute(prop_path.name, type_to_create_if_not_exist, is_custom) + else: + logger.error(f"Property {prop_path} does not exist and 'type_to_create_if_not_exist' was not provided.") + return False + + if not prop: + logger.error(f"Failed to get or create property at path: '{prop_path}'") + return False + + # set the value + if value is None: + return bool(prop.Clear()) + else: + return bool(prop.Set(value, Usd.TimeCode.Default())) + + +""" +Exporting. +""" + + +def export_prim_to_file( + path: str | Sdf.Path, + source_prim_path: str | Sdf.Path, + target_prim_path: str | Sdf.Path | None = None, + stage: Usd.Stage | None = None, +): + """Exports a prim from a given stage to a USD file. + + The function creates a new layer at the provided path and copies the prim to the layer. + It sets the copied prim as the default prim in the target layer. Additionally, it updates + the stage up-axis and meters-per-unit to match the current stage. + + Args: + path: The filepath path to export the prim to. + source_prim_path: The prim path to export. + target_prim_path: The prim path to set as the default prim in the target layer. + Defaults to None, in which case the source prim path is used. + stage: The stage where the prim exists. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: If the prim paths are not global (i.e: do not start with '/'). + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # automatically casting to str in case args + # are path types + path = str(path) + source_prim_path = str(source_prim_path) + if target_prim_path is not None: + target_prim_path = str(target_prim_path) + + if not source_prim_path.startswith("/"): + raise ValueError(f"Source prim path '{source_prim_path}' is not global. It must start with '/'.") + if target_prim_path is not None and not target_prim_path.startswith("/"): + raise ValueError(f"Target prim path '{target_prim_path}' is not global. It must start with '/'.") + + # get root layer + source_layer = stage.GetRootLayer() + + # only create a new layer if it doesn't exist already + target_layer = Sdf.Find(path) + if target_layer is None: + target_layer = Sdf.Layer.CreateNew(path) + # open the target stage + target_stage = Usd.Stage.Open(target_layer) + + # update stage data + UsdGeom.SetStageUpAxis(target_stage, UsdGeom.GetStageUpAxis(stage)) + UsdGeom.SetStageMetersPerUnit(target_stage, UsdGeom.GetStageMetersPerUnit(stage)) + + # specify the prim to copy + source_prim_path = Sdf.Path(source_prim_path) + if target_prim_path is None: + target_prim_path = source_prim_path + + # copy the prim + Sdf.CreatePrimInLayer(target_layer, target_prim_path) + Sdf.CopySpec(source_layer, source_prim_path, target_layer, target_prim_path) + # set the default prim + target_layer.defaultPrim = Sdf.Path(target_prim_path).name + # resolve all paths relative to layer path + omni.usd.resolve_paths(source_layer.identifier, target_layer.identifier) + # save the stage + target_layer.Save() + + """ -Decorators. +Decorators """ @@ -201,7 +650,7 @@ def wrapper(prim_path: str | Sdf.Path, *args, **kwargs): count_success += 1 # check if we were successful in applying the function to any prim if count_success == 0: - omni.log.warn( + logger.warning( f"Could not perform '{func.__name__}' on any prims under: '{prim_path}'." " This might be because of the following reasons:" "\n\t(1) The desired attribute does not exist on any of the prims." @@ -278,23 +727,20 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs): # deal with spaces by replacing them with underscores semantic_type_sanitized = semantic_type.replace(" ", "_") semantic_value_sanitized = semantic_value.replace(" ", "_") - # set the semantic API for the instance - instance_name = f"{semantic_type_sanitized}_{semantic_value_sanitized}" - sem = Semantics.SemanticsAPI.Apply(prim, instance_name) - # create semantic type and data attributes - sem.CreateSemanticTypeAttr() - sem.CreateSemanticDataAttr() - sem.GetSemanticTypeAttr().Set(semantic_type) - sem.GetSemanticDataAttr().Set(semantic_value) - # activate rigid body contact sensors - if hasattr(cfg, "activate_contact_sensors") and cfg.activate_contact_sensors: - schemas.activate_contact_sensors(prim_paths[0], cfg.activate_contact_sensors) + # add labels to the prim + add_labels( + prim, labels=[semantic_value_sanitized], instance_name=semantic_type_sanitized, overwrite=False + ) + # activate rigid body contact sensors (lazy import to avoid circular import with schemas) + if hasattr(cfg, "activate_contact_sensors") and cfg.activate_contact_sensors: # type: ignore + from ..schemas import schemas as _schemas + + _schemas.activate_contact_sensors(prim_paths[0]) # clone asset using cloner API if len(prim_paths) > 1: cloner = Cloner(stage=stage) # check version of Isaac Sim to determine whether clone_in_fabric is valid - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: # clone the prim cloner.clone( prim_paths[0], prim_paths[1:], replicate_physics=False, copy_from_source=cfg.copy_from_source @@ -360,6 +806,7 @@ def bind_visual_material( raise ValueError(f"Visual material '{material_path}' does not exist.") # resolve token for weaker than descendants + # bind material command expects a string token if stronger_than_descendants: binding_strength = "strongerThanDescendants" else: @@ -424,7 +871,7 @@ def bind_physics_material( has_deformable_body = prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI) has_particle_system = prim.IsA(PhysxSchema.PhysxParticleSystem) if not (has_physics_scene_api or has_collider or has_deformable_body or has_particle_system): - omni.log.verbose( + logger.debug( f"Cannot apply physics material '{material_path}' on prim '{prim_path}'. It is neither a" " PhysX scene, collider, a deformable body, nor a particle system." ) @@ -450,514 +897,104 @@ def bind_physics_material( """ -Exporting. +USD References and Variants. """ -def export_prim_to_file( - path: str | Sdf.Path, - source_prim_path: str | Sdf.Path, - target_prim_path: str | Sdf.Path | None = None, - stage: Usd.Stage | None = None, -): - """Exports a prim from a given stage to a USD file. +def add_usd_reference( + prim_path: str, usd_path: str, prim_type: str = "Xform", stage: Usd.Stage | None = None +) -> Usd.Prim: + """Adds a USD reference at the specified prim path on the provided stage. - The function creates a new layer at the provided path and copies the prim to the layer. - It sets the copied prim as the default prim in the target layer. Additionally, it updates - the stage up-axis and meters-per-unit to match the current stage. + This function adds a reference to an external USD file at the specified prim path on the provided stage. + If the prim does not exist, it will be created with the specified type. + + The function also handles stage units verification to ensure compatibility. For instance, + if the current stage is in meters and the referenced USD file is in centimeters, the function will + convert the units to match. This is done using the :mod:`omni.metrics.assembler` functionality. Args: - path: The filepath path to export the prim to. - source_prim_path: The prim path to export. - target_prim_path: The prim path to set as the default prim in the target layer. - Defaults to None, in which case the source prim path is used. - stage: The stage where the prim exists. Defaults to None, in which case the - current stage is used. + prim_path: The prim path where the reference will be attached. + usd_path: The path to USD file to reference. + prim_type: The type of prim to create if it doesn't exist. Defaults to "Xform". + stage: The stage to add the reference to. Defaults to None, in which case the current stage is used. + + Returns: + The USD prim at the specified prim path. Raises: - ValueError: If the prim paths are not global (i.e: do not start with '/'). + FileNotFoundError: When the input USD file is not found at the specified path. """ - # get stage handle - if stage is None: - stage = get_current_stage() + # get current stage + stage = get_current_stage() if stage is None else stage + # get prim at path + prim = stage.GetPrimAtPath(prim_path) + if not prim.IsValid(): + prim = stage.DefinePrim(prim_path, prim_type) + + def _add_reference_to_prim(prim: Usd.Prim) -> Usd.Prim: + """Helper function to add a reference to a prim.""" + success_bool = prim.GetReferences().AddReference(usd_path) + if not success_bool: + raise RuntimeError( + f"Unable to add USD reference to the prim at path: {prim_path} from the USD file at path: {usd_path}" + ) + return prim - # automatically casting to str in case args - # are path types - path = str(path) - source_prim_path = str(source_prim_path) - if target_prim_path is not None: - target_prim_path = str(target_prim_path) + # Compatibility with Isaac Sim 4.5 where omni.metrics is not available + if get_isaac_sim_version().major < 5: + return _add_reference_to_prim(prim) - if not source_prim_path.startswith("/"): - raise ValueError(f"Source prim path '{source_prim_path}' is not global. It must start with '/'.") - if target_prim_path is not None and not target_prim_path.startswith("/"): - raise ValueError(f"Target prim path '{target_prim_path}' is not global. It must start with '/'.") - - # get root layer - source_layer = stage.GetRootLayer() - - # only create a new layer if it doesn't exist already - target_layer = Sdf.Find(path) - if target_layer is None: - target_layer = Sdf.Layer.CreateNew(path) - # open the target stage - target_stage = Usd.Stage.Open(target_layer) - - # update stage data - UsdGeom.SetStageUpAxis(target_stage, UsdGeom.GetStageUpAxis(stage)) - UsdGeom.SetStageMetersPerUnit(target_stage, UsdGeom.GetStageMetersPerUnit(stage)) - - # specify the prim to copy - source_prim_path = Sdf.Path(source_prim_path) - if target_prim_path is None: - target_prim_path = source_prim_path - - # copy the prim - Sdf.CreatePrimInLayer(target_layer, target_prim_path) - Sdf.CopySpec(source_layer, source_prim_path, target_layer, target_prim_path) - # set the default prim - target_layer.defaultPrim = Sdf.Path(target_prim_path).name - # resolve all paths relative to layer path - omni.usd.resolve_paths(source_layer.identifier, target_layer.identifier) - # save the stage - target_layer.Save() - - -""" -USD Prim properties. -""" - - -def make_uninstanceable(prim_path: str | Sdf.Path, stage: Usd.Stage | None = None): - """Check if a prim and its descendants are instanced and make them uninstanceable. - - This function checks if the prim at the specified prim path and its descendants are instanced. - If so, it makes the respective prim uninstanceable by disabling instancing on the prim. - - This is useful when we want to modify the properties of a prim that is instanced. For example, if we - want to apply a different material on an instanced prim, we need to make the prim uninstanceable first. - - Args: - prim_path: The prim path to check. - stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. - - Raises: - ValueError: If the prim path is not global (i.e: does not start with '/'). - """ - # get stage handle - if stage is None: - stage = get_current_stage() - - # make paths str type if they aren't already - prim_path = str(prim_path) - # check if prim path is global - if not prim_path.startswith("/"): - raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - # get prim - prim: Usd.Prim = stage.GetPrimAtPath(prim_path) - # check if prim is valid - if not prim.IsValid(): - raise ValueError(f"Prim at path '{prim_path}' is not valid.") - # iterate over all prims under prim-path - all_prims = [prim] - while len(all_prims) > 0: - # get current prim - child_prim = all_prims.pop(0) - # check if prim is instanced - if child_prim.IsInstance(): - # make the prim uninstanceable - child_prim.SetInstanceable(False) - # add children to list - all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) - - -def resolve_prim_pose( - prim: Usd.Prim, ref_prim: Usd.Prim | None = None -) -> tuple[tuple[float, float, float], tuple[float, float, float, float]]: - """Resolve the pose of a prim with respect to another prim. - - Note: - This function ignores scale and skew by orthonormalizing the transformation - matrix at the final step. However, if any ancestor prim in the hierarchy - has non-uniform scale, that scale will still affect the resulting position - and orientation of the prim (because it's baked into the transform before - scale removal). - - In other words: scale **is not removed hierarchically**. If you need - completely scale-free poses, you must walk the transform chain and strip - scale at each level. Please open an issue if you need this functionality. - - Args: - prim: The USD prim to resolve the pose for. - ref_prim: The USD prim to compute the pose with respect to. - Defaults to None, in which case the world frame is used. - - Returns: - A tuple containing the position (as a 3D vector) and the quaternion orientation - in the (w, x, y, z) format. - - Raises: - ValueError: If the prim or ref prim is not valid. - """ - # check if prim is valid - if not prim.IsValid(): - raise ValueError(f"Prim at path '{prim.GetPath().pathString}' is not valid.") - # get prim xform - xform = UsdGeom.Xformable(prim) - prim_tf = xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) - # sanitize quaternion - # this is needed, otherwise the quaternion might be non-normalized - prim_tf = prim_tf.GetOrthonormalized() - - if ref_prim is not None: - # check if ref prim is valid - if not ref_prim.IsValid(): - raise ValueError(f"Ref prim at path '{ref_prim.GetPath().pathString}' is not valid.") - # get ref prim xform - ref_xform = UsdGeom.Xformable(ref_prim) - ref_tf = ref_xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) - # make sure ref tf is orthonormal - ref_tf = ref_tf.GetOrthonormalized() - # compute relative transform to get prim in ref frame - prim_tf = prim_tf * ref_tf.GetInverse() - - # extract position and orientation - prim_pos = [*prim_tf.ExtractTranslation()] - prim_quat = [prim_tf.ExtractRotationQuat().real, *prim_tf.ExtractRotationQuat().imaginary] - return tuple(prim_pos), tuple(prim_quat) - - -def resolve_prim_scale(prim: Usd.Prim) -> tuple[float, float, float]: - """Resolve the scale of a prim in the world frame. - - At an attribute level, a USD prim's scale is a scaling transformation applied to the prim with - respect to its parent prim. This function resolves the scale of the prim in the world frame, - by computing the local to world transform of the prim. This is equivalent to traversing up - the prim hierarchy and accounting for the rotations and scales of the prims. - - For instance, if a prim has a scale of (1, 2, 3) and it is a child of a prim with a scale of (4, 5, 6), - then the scale of the prim in the world frame is (4, 10, 18). - - Args: - prim: The USD prim to resolve the scale for. - - Returns: - The scale of the prim in the x, y, and z directions in the world frame. - - Raises: - ValueError: If the prim is not valid. - """ - # check if prim is valid - if not prim.IsValid(): - raise ValueError(f"Prim at path '{prim.GetPath().pathString}' is not valid.") - # compute local to world transform - xform = UsdGeom.Xformable(prim) - world_transform = xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) - # extract scale - return tuple([*(v.GetLength() for v in world_transform.ExtractRotationMatrix())]) - - -""" -USD Stage traversal. -""" - - -def get_first_matching_child_prim( - prim_path: str | Sdf.Path, - predicate: Callable[[Usd.Prim], bool], - stage: Usd.Stage | None = None, - traverse_instance_prims: bool = True, -) -> Usd.Prim | None: - """Recursively get the first USD Prim at the path string that passes the predicate function. - - This function performs a depth-first traversal of the prim hierarchy starting from - :attr:`prim_path`, returning the first prim that satisfies the provided :attr:`predicate`. - It optionally supports traversal through instance prims, which are normally skipped in standard USD - traversals. - - USD instance prims are lightweight copies of prototype scene structures and are not included - in default traversals unless explicitly handled. This function allows traversing into instances - when :attr:`traverse_instance_prims` is set to :attr:`True`. - - .. versionchanged:: 2.3.0 - - Added :attr:`traverse_instance_prims` to control whether to traverse instance prims. - By default, instance prims are now traversed. - - Args: - prim_path: The path of the prim in the stage. - predicate: The function to test the prims against. It takes a prim as input and returns a boolean. - stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. - traverse_instance_prims: Whether to traverse instance prims. Defaults to True. - - Returns: - The first prim on the path that passes the predicate. If no prim passes the predicate, it returns None. - - Raises: - ValueError: If the prim path is not global (i.e: does not start with '/'). - """ - # get stage handle - if stage is None: - stage = get_current_stage() - - # make paths str type if they aren't already - prim_path = str(prim_path) - # check if prim path is global - if not prim_path.startswith("/"): - raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - # get prim - prim = stage.GetPrimAtPath(prim_path) - # check if prim is valid - if not prim.IsValid(): - raise ValueError(f"Prim at path '{prim_path}' is not valid.") - # iterate over all prims under prim-path - all_prims = [prim] - while len(all_prims) > 0: - # get current prim - child_prim = all_prims.pop(0) - # check if prim passes predicate - if predicate(child_prim): - return child_prim - # add children to list - if traverse_instance_prims: - all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) - else: - all_prims += child_prim.GetChildren() - return None - - -def get_all_matching_child_prims( - prim_path: str | Sdf.Path, - predicate: Callable[[Usd.Prim], bool] = lambda _: True, - depth: int | None = None, - stage: Usd.Stage | None = None, - traverse_instance_prims: bool = True, -) -> list[Usd.Prim]: - """Performs a search starting from the root and returns all the prims matching the predicate. - - This function performs a depth-first traversal of the prim hierarchy starting from - :attr:`prim_path`, returning all prims that satisfy the provided :attr:`predicate`. It optionally - supports traversal through instance prims, which are normally skipped in standard USD traversals. - - USD instance prims are lightweight copies of prototype scene structures and are not included - in default traversals unless explicitly handled. This function allows traversing into instances - when :attr:`traverse_instance_prims` is set to :attr:`True`. - - .. versionchanged:: 2.3.0 - - Added :attr:`traverse_instance_prims` to control whether to traverse instance prims. - By default, instance prims are now traversed. - - Args: - prim_path: The root prim path to start the search from. - predicate: The predicate that checks if the prim matches the desired criteria. It takes a prim as input - and returns a boolean. Defaults to a function that always returns True. - depth: The maximum depth for traversal, should be bigger than zero if specified. - Defaults to None (i.e: traversal happens till the end of the tree). - stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. - traverse_instance_prims: Whether to traverse instance prims. Defaults to True. - - Returns: - A list containing all the prims matching the predicate. - - Raises: - ValueError: If the prim path is not global (i.e: does not start with '/'). - """ - # get stage handle - if stage is None: - stage = get_current_stage() - - # make paths str type if they aren't already - prim_path = str(prim_path) - # check if prim path is global - if not prim_path.startswith("/"): - raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - # get prim - prim = stage.GetPrimAtPath(prim_path) - # check if prim is valid - if not prim.IsValid(): - raise ValueError(f"Prim at path '{prim_path}' is not valid.") - # check if depth is valid - if depth is not None and depth <= 0: - raise ValueError(f"Depth must be bigger than zero, got {depth}.") - - # iterate over all prims under prim-path - # list of tuples (prim, current_depth) - all_prims_queue = [(prim, 0)] - output_prims = [] - while len(all_prims_queue) > 0: - # get current prim - child_prim, current_depth = all_prims_queue.pop(0) - # check if prim passes predicate - if predicate(child_prim): - output_prims.append(child_prim) - # add children to list - if depth is None or current_depth < depth: - # resolve prims under the current prim - if traverse_instance_prims: - children = child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) - else: - children = child_prim.GetChildren() - # add children to list - all_prims_queue += [(child, current_depth + 1) for child in children] - - return output_prims - - -def find_first_matching_prim(prim_path_regex: str, stage: Usd.Stage | None = None) -> Usd.Prim | None: - """Find the first matching prim in the stage based on input regex expression. - - Args: - prim_path_regex: The regex expression for prim path. - stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. - - Returns: - The first prim that matches input expression. If no prim matches, returns None. - - Raises: - ValueError: If the prim path is not global (i.e: does not start with '/'). - """ - # get stage handle - if stage is None: - stage = get_current_stage() - - # check prim path is global - if not prim_path_regex.startswith("/"): - raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") - # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string - pattern = f"^{prim_path_regex}$" - compiled_pattern = re.compile(pattern) - # obtain matching prim (depth-first search) - for prim in stage.Traverse(): - # check if prim passes predicate - if compiled_pattern.match(prim.GetPath().pathString) is not None: - return prim - return None - - -def find_matching_prims(prim_path_regex: str, stage: Usd.Stage | None = None) -> list[Usd.Prim]: - """Find all the matching prims in the stage based on input regex expression. - - Args: - prim_path_regex: The regex expression for prim path. - stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. - - Returns: - A list of prims that match input expression. - - Raises: - ValueError: If the prim path is not global (i.e: does not start with '/'). - """ - # get stage handle - if stage is None: - stage = get_current_stage() - - # check prim path is global - if not prim_path_regex.startswith("/"): - raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") - # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string - tokens = prim_path_regex.split("/")[1:] - tokens = [f"^{token}$" for token in tokens] - # iterate over all prims in stage (breath-first search) - all_prims = [stage.GetPseudoRoot()] - output_prims = [] - for index, token in enumerate(tokens): - token_compiled = re.compile(token) - for prim in all_prims: - for child in prim.GetAllChildren(): - if token_compiled.match(child.GetName()) is not None: - output_prims.append(child) - if index < len(tokens) - 1: - all_prims = output_prims - output_prims = [] - return output_prims - - -def find_matching_prim_paths(prim_path_regex: str, stage: Usd.Stage | None = None) -> list[str]: - """Find all the matching prim paths in the stage based on input regex expression. - - Args: - prim_path_regex: The regex expression for prim path. - stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. - - Returns: - A list of prim paths that match input expression. - - Raises: - ValueError: If the prim path is not global (i.e: does not start with '/'). - """ - # obtain matching prims - output_prims = find_matching_prims(prim_path_regex, stage) - # convert prims to prim paths - output_prim_paths = [] - for prim in output_prims: - output_prim_paths.append(prim.GetPath().pathString) - return output_prim_paths + # check if the USD file is valid and add reference to the prim + sdf_layer = Sdf.Layer.FindOrOpen(usd_path) + if not sdf_layer: + raise FileNotFoundError(f"Unable to open the usd file at path: {usd_path}") + # import metrics assembler interface + # note: this is only available in Isaac Sim 5.0 and above + from omni.metrics.assembler.core import get_metrics_assembler_interface -def find_global_fixed_joint_prim( - prim_path: str | Sdf.Path, check_enabled_only: bool = False, stage: Usd.Stage | None = None -) -> UsdPhysics.Joint | None: - """Find the fixed joint prim under the specified prim path that connects the target to the simulation world. + # obtain the stage ID + stage_id = get_current_stage_id() + # check if the layers are compatible (i.e. the same units) + ret_val = get_metrics_assembler_interface().check_layers( + stage.GetRootLayer().identifier, sdf_layer.identifier, stage_id + ) + # log that metric assembler did not detect any issues + if ret_val["ret_val"]: + logger.info( + "Metric assembler detected no issues between the current stage and the referenced USD file at path:" + f" {usd_path}" + ) + # add reference to the prim + return _add_reference_to_prim(prim) - A joint is a connection between two bodies. A fixed joint is a joint that does not allow relative motion - between the two bodies. When a fixed joint has only one target body, it is considered to attach the body - to the simulation world. - This function finds the fixed joint prim that has only one target under the specified prim path. If no such - fixed joint prim exists, it returns None. +def get_usd_references(prim_path: str, stage: Usd.Stage | None = None) -> list[str]: + """Gets the USD references at the specified prim path on the provided stage. Args: - prim_path: The prim path to search for the fixed joint prim. - check_enabled_only: Whether to consider only enabled fixed joints. Defaults to False. - If False, then all joints (enabled or disabled) are considered. - stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + prim_path: The prim path to get the USD references from. + stage: The stage to get the USD references from. Defaults to None, in which case the current stage is used. Returns: - The fixed joint prim that has only one target. If no such fixed joint prim exists, it returns None. + A list of USD reference paths. Raises: - ValueError: If the prim path is not global (i.e: does not start with '/'). - ValueError: If the prim path does not exist on the stage. + ValueError: If the prim at the specified path is not valid. """ # get stage handle - if stage is None: - stage = get_current_stage() - - # check prim path is global - if not prim_path.startswith("/"): - raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - - # check if prim exists + stage = get_current_stage() if stage is None else stage + # get prim at path prim = stage.GetPrimAtPath(prim_path) if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") - - fixed_joint_prim = None - # we check all joints under the root prim and classify the asset as fixed base if there exists - # a fixed joint that has only one target (i.e. the root link). - for prim in Usd.PrimRange(prim): - # note: ideally checking if it is FixedJoint would have been enough, but some assets use "Joint" as the - # schema name which makes it difficult to distinguish between the two. - joint_prim = UsdPhysics.Joint(prim) - if joint_prim: - # if check_enabled_only is True, we only consider enabled joints - if check_enabled_only and not joint_prim.GetJointEnabledAttr().Get(): - continue - # check body 0 and body 1 exist - body_0_exist = joint_prim.GetBody0Rel().GetTargets() != [] - body_1_exist = joint_prim.GetBody1Rel().GetTargets() != [] - # if either body 0 or body 1 does not exist, we have a fixed joint that connects to the world - if not (body_0_exist and body_1_exist): - fixed_joint_prim = joint_prim - break - - return fixed_joint_prim - - -""" -USD Variants. -""" + # get USD references + references = [] + for prim_spec in prim.GetPrimStack(): + for ref in prim_spec.referenceList.prependedItems: + references.append(str(ref.assetPath)) + return references def select_usd_variants(prim_path: str, variants: object | dict[str, str], stage: Usd.Stage | None = None): @@ -991,6 +1028,7 @@ class TableVariants: color: Literal["blue", "red"] = "red" size: Literal["small", "large"] = "large" + select_usd_variants( prim_path="/World/Table", variants=TableVariants(), @@ -1016,166 +1054,76 @@ class TableVariants: raise ValueError(f"Prim at path '{prim_path}' is not valid.") # Convert to dict if we have a configclass object. if not isinstance(variants, dict): - variants = variants.to_dict() + variants = variants.to_dict() # type: ignore existing_variant_sets = prim.GetVariantSets() - for variant_set_name, variant_selection in variants.items(): + for variant_set_name, variant_selection in variants.items(): # type: ignore # Check if the variant set exists on the prim. if not existing_variant_sets.HasVariantSet(variant_set_name): - omni.log.warn(f"Variant set '{variant_set_name}' does not exist on prim '{prim_path}'.") + logger.warning(f"Variant set '{variant_set_name}' does not exist on prim '{prim_path}'.") continue variant_set = existing_variant_sets.GetVariantSet(variant_set_name) # Only set the variant selection if it is different from the current selection. if variant_set.GetVariantSelection() != variant_selection: variant_set.SetVariantSelection(variant_selection) - omni.log.info( + logger.info( f"Setting variant selection '{variant_selection}' for variant set '{variant_set_name}' on" f" prim '{prim_path}'." ) """ -Stage management. +Internal Helpers. """ -def attach_stage_to_usd_context(attaching_early: bool = False): - """Attaches the current USD stage in memory to the USD context. - - This function should be called during or after scene is created and before stage is simulated or rendered. +def _to_tuple(value: Any) -> tuple[float, ...]: + """Convert various sequence types to a Python tuple of floats. - Note: - If the stage is not in memory or rendering is not enabled, this function will return without attaching. + This function provides robust conversion from different array-like types (list, tuple, numpy array, + torch tensor) to Python tuples. It handles edge cases like malformed sequences, CUDA tensors, + and arrays with singleton dimensions. Args: - attaching_early: Whether to attach the stage to the usd context before stage is created. Defaults to False. - """ - - from isaacsim.core.simulation_manager import SimulationManager - - from isaaclab.sim.simulation_context import SimulationContext - - # if Isaac Sim version is less than 5.0, stage in memory is not supported - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: - return - - # if stage is not in memory, we can return early - if not is_current_stage_in_memory(): - return - - # attach stage to physx - stage_id = get_current_stage_id() - physx_sim_interface = omni.physx.get_physx_simulation_interface() - physx_sim_interface.attach_stage(stage_id) - - # this carb flag is equivalent to if rendering is enabled - carb_setting = carb.settings.get_settings() - is_rendering_enabled = get_carb_setting(carb_setting, "/physics/fabricUpdateTransformations") - - # if rendering is not enabled, we don't need to attach it - if not is_rendering_enabled: - return - - # early attach warning msg - if attaching_early: - omni.log.warn( - "Attaching stage in memory to USD context early to support an operation which doesn't support stage in" - " memory." - ) - - # skip this callback to avoid wiping the stage after attachment - SimulationContext.instance().skip_next_stage_open_callback() - - # disable stage open callback to avoid clearing callbacks - SimulationManager.enable_stage_open_callback(False) - - # enable physics fabric - SimulationContext.instance()._physics_context.enable_fabric(True) - - # attach stage to usd context - omni.usd.get_context().attach_stage_with_callback(stage_id) - - # attach stage to physx - physx_sim_interface = omni.physx.get_physx_simulation_interface() - physx_sim_interface.attach_stage(stage_id) - - # re-enable stage open callback - SimulationManager.enable_stage_open_callback(True) - - -def is_current_stage_in_memory() -> bool: - """Checks if the current stage is in memory. - - This function compares the stage id of the current USD stage with the stage id of the USD context stage. - - Returns: - Whether the current stage is in memory. - """ - - # grab current stage id - stage_id = get_current_stage_id() - - # grab context stage id - context_stage = omni.usd.get_context().get_stage() - with use_stage(context_stage): - context_stage_id = get_current_stage_id() - - # check if stage ids are the same - return stage_id != context_stage_id - - -@contextlib.contextmanager -def use_stage(stage: Usd.Stage) -> Generator[None, None, None]: - """Context manager that sets a thread-local stage, if supported. - - In Isaac Sim < 5.0, this is a no-op to maintain compatibility. - - Args: - stage: The stage to set temporarily. - - Yields: - None - """ - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: - omni.log.warn("[Compat] Isaac Sim < 5.0 does not support thread-local stage contexts. Skipping use_stage().") - yield # no-op - else: - with stage_utils.use_stage(stage): - yield - - -def create_new_stage_in_memory() -> Usd.Stage: - """Creates a new stage in memory, if supported. + value: A sequence-like object containing floats. Supported types include: + - Python list or tuple + - NumPy array (any device) + - PyTorch tensor (CPU or CUDA) + - Mixed sequences with numpy/torch scalar items and float values Returns: - The new stage in memory. - """ - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: - omni.log.warn( - "[Compat] Isaac Sim < 5.0 does not support creating a new stage in memory. Falling back to creating a new" - " stage attached to USD context." - ) - return stage_utils.create_new_stage() - else: - return stage_utils.create_new_stage_in_memory() - - -def get_current_stage_id() -> int: - """Gets the current open stage id. + A one-dimensional tuple of floats. - This function is a reimplementation of :meth:`isaacsim.core.utils.stage.get_current_stage_id` for - backwards compatibility to Isaac Sim < 5.0. + Raises: + ValueError: If the input value is not one-dimensional after squeezing singleton dimensions. + + Example: + >>> import torch + >>> import numpy as np + >>> + >>> _to_tuple([1.0, 2.0, 3.0]) + (1.0, 2.0, 3.0) + >>> _to_tuple(torch.tensor([[1.0, 2.0]])) # Squeezes first dimension + (1.0, 2.0) + >>> _to_tuple(np.array([1.0, 2.0, 3.0])) + (1.0, 2.0, 3.0) + >>> _to_tuple((1.0, 2.0, 3.0)) + (1.0, 2.0, 3.0) - Returns: - The current open stage id. """ - stage = get_current_stage() - stage_cache = UsdUtils.StageCache.Get() - stage_id = stage_cache.GetId(stage).ToLongInt() - if stage_id < 0: - stage_id = stage_cache.Insert(stage).ToLongInt() - return stage_id + # Normalize to tensor if value is a plain sequence (list with mixed types, etc.) + # This handles cases like [np.float32(1.0), 2.0, torch.tensor(3.0)] + if not hasattr(value, "tolist"): + value = torch.tensor(value, device="cpu", dtype=torch.float) + + # Remove leading singleton dimension if present (e.g., shape (1, 3) -> (3,)) + # This is common when batched operations produce single-item batches + if value.ndim != 1: + value = value.squeeze() + # Validate that the result is one-dimensional + if value.ndim != 1: + raise ValueError(f"Input value is not one dimensional: {value.shape}") + + # Convert to tuple - works for both numpy arrays and torch tensors + return tuple(value.tolist()) diff --git a/source/isaaclab/isaaclab/sim/utils/queries.py b/source/isaaclab/isaaclab/sim/utils/queries.py new file mode 100644 index 000000000000..035681a726b4 --- /dev/null +++ b/source/isaaclab/isaaclab/sim/utils/queries.py @@ -0,0 +1,407 @@ +# 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 + +"""Utilities for querying the USD stage.""" + +from __future__ import annotations + +import logging +import re +from collections.abc import Callable + +import omni +import omni.kit.app +from pxr import Sdf, Usd, UsdPhysics + +from .stage import get_current_stage + +# import logger +logger = logging.getLogger(__name__) + + +def get_next_free_prim_path(path: str, stage: Usd.Stage | None = None) -> str: + """Gets a new prim path that doesn't exist in the stage given a base path. + + If the given path doesn't exist in the stage already, it returns the given path. Otherwise, + it appends a suffix with an incrementing number to the given path. + + Args: + path: The base prim path to check. + stage: The stage to check. Defaults to the current stage. + + Returns: + A new path that is guaranteed to not exist on the current stage + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> # given the stage: /World/Cube, /World/Cube_01. + >>> # Get the next available path for /World/Cube + >>> sim_utils.get_next_free_prim_path("/World/Cube") + /World/Cube_02 + """ + # get current stage + stage = get_current_stage() if stage is None else stage + # get next free path + return omni.usd.get_stage_next_free_path(stage, path, True) + + +def get_first_matching_ancestor_prim( + prim_path: str | Sdf.Path, + predicate: Callable[[Usd.Prim], bool], + stage: Usd.Stage | None = None, +) -> Usd.Prim | None: + """Gets the first ancestor prim that passes the predicate function. + + This function walks up the prim hierarchy starting from the target prim and returns the first ancestor prim + that passes the predicate function. This includes the prim itself if it passes the predicate. + + Args: + prim_path: The path of the prim in the stage. + predicate: The function to test the prims against. It takes a prim as input and returns a boolean. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + The first ancestor prim that passes the predicate. If no ancestor prim passes the predicate, it returns None. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # make paths str type if they aren't already + prim_path = str(prim_path) + # check if prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + + # walk up to find the first matching ancestor prim + ancestor_prim = prim + while ancestor_prim and ancestor_prim.IsValid(): + # check if prim passes predicate + if predicate(ancestor_prim): + return ancestor_prim + # get parent prim + ancestor_prim = ancestor_prim.GetParent() + + # If no ancestor prim passes the predicate, return None + return None + + +def get_first_matching_child_prim( + prim_path: str | Sdf.Path, + predicate: Callable[[Usd.Prim], bool], + stage: Usd.Stage | None = None, + traverse_instance_prims: bool = True, +) -> Usd.Prim | None: + """Recursively get the first USD Prim at the path string that passes the predicate function. + + This function performs a depth-first traversal of the prim hierarchy starting from + :attr:`prim_path`, returning the first prim that satisfies the provided :attr:`predicate`. + It optionally supports traversal through instance prims, which are normally skipped in standard USD + traversals. + + USD instance prims are lightweight copies of prototype scene structures and are not included + in default traversals unless explicitly handled. This function allows traversing into instances + when :attr:`traverse_instance_prims` is set to :attr:`True`. + + .. versionchanged:: 2.3.0 + + Added :attr:`traverse_instance_prims` to control whether to traverse instance prims. + By default, instance prims are now traversed. + + Args: + prim_path: The path of the prim in the stage. + predicate: The function to test the prims against. It takes a prim as input and returns a boolean. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + traverse_instance_prims: Whether to traverse instance prims. Defaults to True. + + Returns: + The first prim on the path that passes the predicate. If no prim passes the predicate, it returns None. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # make paths str type if they aren't already + prim_path = str(prim_path) + # check if prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # iterate over all prims under prim-path + all_prims = [prim] + while len(all_prims) > 0: + # get current prim + child_prim = all_prims.pop(0) + # check if prim passes predicate + if predicate(child_prim): + return child_prim + # add children to list + if traverse_instance_prims: + all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) + else: + all_prims += child_prim.GetChildren() + return None + + +def get_all_matching_child_prims( + prim_path: str | Sdf.Path, + predicate: Callable[[Usd.Prim], bool] = lambda _: True, + depth: int | None = None, + stage: Usd.Stage | None = None, + traverse_instance_prims: bool = True, +) -> list[Usd.Prim]: + """Performs a search starting from the root and returns all the prims matching the predicate. + + This function performs a depth-first traversal of the prim hierarchy starting from + :attr:`prim_path`, returning all prims that satisfy the provided :attr:`predicate`. It optionally + supports traversal through instance prims, which are normally skipped in standard USD traversals. + + USD instance prims are lightweight copies of prototype scene structures and are not included + in default traversals unless explicitly handled. This function allows traversing into instances + when :attr:`traverse_instance_prims` is set to :attr:`True`. + + .. versionchanged:: 2.3.0 + + Added :attr:`traverse_instance_prims` to control whether to traverse instance prims. + By default, instance prims are now traversed. + + Args: + prim_path: The root prim path to start the search from. + predicate: The predicate that checks if the prim matches the desired criteria. It takes a prim as input + and returns a boolean. Defaults to a function that always returns True. + depth: The maximum depth for traversal, should be bigger than zero if specified. + Defaults to None (i.e: traversal happens till the end of the tree). + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + traverse_instance_prims: Whether to traverse instance prims. Defaults to True. + + Returns: + A list containing all the prims matching the predicate. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # make paths str type if they aren't already + prim_path = str(prim_path) + # check if prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # check if depth is valid + if depth is not None and depth <= 0: + raise ValueError(f"Depth must be bigger than zero, got {depth}.") + + # iterate over all prims under prim-path + # list of tuples (prim, current_depth) + all_prims_queue = [(prim, 0)] + output_prims = [] + while len(all_prims_queue) > 0: + # get current prim + child_prim, current_depth = all_prims_queue.pop(0) + # check if prim passes predicate + if predicate(child_prim): + output_prims.append(child_prim) + # add children to list + if depth is None or current_depth < depth: + # resolve prims under the current prim + if traverse_instance_prims: + children = child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) + else: + children = child_prim.GetChildren() + # add children to list + all_prims_queue += [(child, current_depth + 1) for child in children] + + return output_prims + + +def find_first_matching_prim(prim_path_regex: str, stage: Usd.Stage | None = None) -> Usd.Prim | None: + """Find the first matching prim in the stage based on input regex expression. + + Args: + prim_path_regex: The regex expression for prim path. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + The first prim that matches input expression. If no prim matches, returns None. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # check prim path is global + if not prim_path_regex.startswith("/"): + raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") + prim_path_regex = _normalize_legacy_wildcard_pattern(prim_path_regex) + # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string + pattern = f"^{prim_path_regex}$" + compiled_pattern = re.compile(pattern) + # obtain matching prim (depth-first search) + for prim in stage.Traverse(): + # check if prim passes predicate + if compiled_pattern.match(prim.GetPath().pathString) is not None: + return prim + return None + + +def _normalize_legacy_wildcard_pattern(prim_path_regex: str) -> str: + """Convert legacy '*' wildcard usage to '.*' and warn users.""" + fixed_regex = re.sub(r"(? list[Usd.Prim]: + """Find all the matching prims in the stage based on input regex expression. + + Args: + prim_path_regex: The regex expression for prim path. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + A list of prims that match input expression. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # normalize legacy wildcard pattern + prim_path_regex = _normalize_legacy_wildcard_pattern(prim_path_regex) + + # check prim path is global + if not prim_path_regex.startswith("/"): + raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") + # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string + tokens = prim_path_regex.split("/")[1:] + tokens = [f"^{token}$" for token in tokens] + # iterate over all prims in stage (breath-first search) + all_prims = [stage.GetPseudoRoot()] + output_prims = [] + for index, token in enumerate(tokens): + token_compiled = re.compile(token) + for prim in all_prims: + for child in prim.GetAllChildren(): + if token_compiled.match(child.GetName()) is not None: + output_prims.append(child) + if index < len(tokens) - 1: + all_prims = output_prims + output_prims = [] + return output_prims + + +def find_matching_prim_paths(prim_path_regex: str, stage: Usd.Stage | None = None) -> list[str]: + """Find all the matching prim paths in the stage based on input regex expression. + + Args: + prim_path_regex: The regex expression for prim path. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + A list of prim paths that match input expression. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # obtain matching prims + output_prims = find_matching_prims(prim_path_regex, stage) + # convert prims to prim paths + output_prim_paths = [] + for prim in output_prims: + output_prim_paths.append(prim.GetPath().pathString) + return output_prim_paths + + +def find_global_fixed_joint_prim( + prim_path: str | Sdf.Path, check_enabled_only: bool = False, stage: Usd.Stage | None = None +) -> UsdPhysics.Joint | None: + """Find the fixed joint prim under the specified prim path that connects the target to the simulation world. + + A joint is a connection between two bodies. A fixed joint is a joint that does not allow relative motion + between the two bodies. When a fixed joint has only one target body, it is considered to attach the body + to the simulation world. + + This function finds the fixed joint prim that has only one target under the specified prim path. If no such + fixed joint prim exists, it returns None. + + Args: + prim_path: The prim path to search for the fixed joint prim. + check_enabled_only: Whether to consider only enabled fixed joints. Defaults to False. + If False, then all joints (enabled or disabled) are considered. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + The fixed joint prim that has only one target. If no such fixed joint prim exists, it returns None. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + ValueError: If the prim path does not exist on the stage. + """ + # get stage handle + if stage is None: + stage = get_current_stage() + + # check prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + + # check if prim exists + prim = stage.GetPrimAtPath(prim_path) + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + + fixed_joint_prim = None + # we check all joints under the root prim and classify the asset as fixed base if there exists + # a fixed joint that has only one target (i.e. the root link). + for prim in Usd.PrimRange(prim): + # note: ideally checking if it is FixedJoint would have been enough, but some assets use "Joint" as the + # schema name which makes it difficult to distinguish between the two. + joint_prim = UsdPhysics.Joint(prim) + if joint_prim: + # if check_enabled_only is True, we only consider enabled joints + if check_enabled_only and not joint_prim.GetJointEnabledAttr().Get(): + continue + # check body 0 and body 1 exist + body_0_exist = joint_prim.GetBody0Rel().GetTargets() != [] + body_1_exist = joint_prim.GetBody1Rel().GetTargets() != [] + # if either body 0 or body 1 does not exist, we have a fixed joint that connects to the world + if not (body_0_exist and body_1_exist): + fixed_joint_prim = joint_prim + break + + return fixed_joint_prim diff --git a/source/isaaclab/isaaclab/sim/utils/semantics.py b/source/isaaclab/isaaclab/sim/utils/semantics.py new file mode 100644 index 000000000000..463eb8b5d42f --- /dev/null +++ b/source/isaaclab/isaaclab/sim/utils/semantics.py @@ -0,0 +1,232 @@ +# 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 + +"""Utilities for applying and removing semantic labels to USD prims.""" + +from __future__ import annotations + +import contextlib +import logging + +from pxr import Usd, UsdGeom + +# USD Semantics is only available in Isaac Sim 5.0 and later. +with contextlib.suppress(ModuleNotFoundError, ImportError): + from pxr import UsdSemantics + +from isaaclab.utils.version import get_isaac_sim_version + +from .stage import get_current_stage + +# import logger +logger = logging.getLogger(__name__) + + +def add_labels(prim: Usd.Prim, labels: list[str], instance_name: str = "class", overwrite: bool = True) -> None: + """Apply semantic labels to a prim using the :class:`UsdSemantics.LabelsAPI`. + + This function is a wrapper around the :func:`omni.replicator.core.functional.modify.semantics` function. + It applies the labels to the prim using the :class:`UsdSemantics.LabelsAPI`. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later, which introduces the :class:`UsdSemantics.LabelsAPI`. + For previous versions, the function falls back to use the deprecated :class:`UsdSemantics.SemanticsAPI` instead. + + Example: + >>> prim = sim_utils.create_prim("/World/Test/Sphere", "Sphere", stage=stage, attributes={"radius": 10.0}) + >>> sim_utils.add_labels(prim, labels=["sphere"], instance_name="class") + + Args: + prim: The USD prim to add or update labels on. + labels: The list of labels to apply. + instance_name: The name of the semantic instance. Defaults to "class". + overwrite: Whether to overwrite existing labels for this instance. If False, + the new labels are appended to existing ones (if any). Defaults to True. + """ + # Try modern approach (Isaac Sim >= 5.0) + try: + import omni.replicator.core.functional as rep_functional + + mode = "replace" if overwrite else "add" + rep_functional.modify.semantics(prim, {instance_name: labels}, mode=mode) + + return + except (ModuleNotFoundError, ImportError) as e: + # check if we are using isaac sim 5.0 + if get_isaac_sim_version().major >= 5: + logger.warning( + f"Failed to add labels to prim {prim.GetPath()} using Replicator API: {e}. " + "\nPlease ensure Replicator API is enabled by passing '--enable_cameras' to the AppLauncher." + "\nFalling back to legacy approach." + ) + + # Try legacy approach (Isaac Sim < 5.0) + try: + import Semantics + + # check we have only one label + if len(labels) != 1: + raise ValueError(f"Only one label can be applied to a prim. Received: {labels}") + # set the semantic API for the instance + instance_name = f"{instance_name}_{labels[0]}" + sem = Semantics.SemanticsAPI.Apply(prim, instance_name) + # create semantic type and data attributes + sem.CreateSemanticTypeAttr() + sem.CreateSemanticDataAttr() + sem.GetSemanticTypeAttr().Set(instance_name) + sem.GetSemanticDataAttr().Set(labels[0]) + except Exception as e: + logger.warning( + f"Failed to add labels to prim {prim.GetPath()} using legacy API: {e}. " + "\nSemantics functionality may not be available in this Isaac Sim version." + " Please open an issue at https://github.com/isaac-sim/IsaacLab/issues if you believe this is a bug." + ) + + +def get_labels(prim: Usd.Prim) -> dict[str, list[str]]: + """Get all semantic labels (:class:`UsdSemantics.LabelsAPI`) applied to a prim. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later. For previous versions, + please use :mod:`isaacsim.core.utils.semantics` module instead. + + Args: + prim: The USD prim to return labels for. + + Returns: + A dictionary mapping instance names to a list of labels. + If no labels are found, it returns an empty dictionary. + """ + result = {} + for schema_name in prim.GetAppliedSchemas(): + if schema_name.startswith("SemanticsLabelsAPI:"): + instance_name = schema_name.split(":", 1)[1] + sem_api = UsdSemantics.LabelsAPI(prim, instance_name) + labels_attr = sem_api.GetLabelsAttr() + if labels_attr: + labels = labels_attr.Get() + result[instance_name] = list(labels) if labels is not None else [] + else: + result[instance_name] = [] + return result + + +def remove_labels(prim: Usd.Prim, instance_name: str | None = None, include_descendants: bool = False): + """Removes semantic labels (:class:`UsdSemantics.LabelsAPI`) from a prim and optionally its descendants. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later. For previous versions, + please use :mod:`isaacsim.core.utils.semantics` module instead. + + Args: + prim: The USD prim to remove labels from. + instance_name: The specific instance name to remove. Defaults to None, in which case + *all* labels are removed. + include_descendants: Whether to also traverse children and remove labels recursively. + Defaults to False. + """ + + def _remove_single_prim_labels(target_prim: Usd.Prim): + """Helper function to remove labels from a single prim.""" + schemas_to_remove = [] + for schema_name in target_prim.GetAppliedSchemas(): + if schema_name.startswith("SemanticsLabelsAPI:"): + current_instance = schema_name.split(":", 1)[1] + if instance_name is None or current_instance == instance_name: + schemas_to_remove.append(current_instance) + + for inst_to_remove in schemas_to_remove: + target_prim.RemoveAPI(UsdSemantics.LabelsAPI, inst_to_remove) + + if include_descendants: + for p in Usd.PrimRange(prim): + _remove_single_prim_labels(p) + else: + _remove_single_prim_labels(prim) + + +def check_missing_labels(prim_path: str | None = None, stage: Usd.Stage | None = None) -> list[str]: + """Checks whether the prim and its descendants at the provided path have missing + semantic labels (:class:`UsdSemantics.LabelsAPI`). + + .. note:: + The function checks only prims that are :class:`UsdGeom.Gprim` type. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later. For previous versions, + please use :mod:`isaacsim.core.utils.semantics` module instead. + + Args: + prim_path: The prim path to search from. If None, the entire stage is inspected. + stage: The stage to search from. If None, the current stage is used. + + Returns: + A list containing prim paths to prims with no labels applied. + """ + # check if stage is valid + stage = stage if stage else get_current_stage() + + # check if inspect path is valid + start_prim = stage.GetPrimAtPath(prim_path) if prim_path else stage.GetPseudoRoot() + if not start_prim: + # Allow None prim_path for whole stage check, warn if path specified but not found + if prim_path: + logger.warning(f"No prim found at path '{prim_path}'. Returning from check for semantic labels.") + return [] + + # iterate over prim and its children + prim_paths = [] + for prim in Usd.PrimRange(start_prim): + if prim.IsA(UsdGeom.Gprim): + has_any_label = False + for schema_name in prim.GetAppliedSchemas(): + if schema_name.startswith("SemanticsLabelsAPI:"): + has_any_label = True + break + if not has_any_label: + prim_paths.append(prim.GetPath().pathString) + + return prim_paths + + +def count_total_labels(prim_path: str | None = None, stage: Usd.Stage | None = None) -> dict[str, int]: + """Counts the number of semantic labels (:class:`UsdSemantics.LabelsAPI`) applied to the prims at the provided path. + + This function iterates over all the prims from the provided path and counts the number of times + each label is applied to the prims. It returns a dictionary of labels and their corresponding count. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later. For previous versions, + please use :mod:`isaacsim.core.utils.semantics` module instead. + + Args: + prim_path: The prim path to search from. If None, the entire stage is inspected. + stage: The stage to search from. If None, the current stage is used. + + Returns: + A dictionary mapping individual labels to their total count across all instances. + The dictionary includes a 'missing_labels' count for prims with no labels. + """ + stage = stage if stage else get_current_stage() + + start_prim = stage.GetPrimAtPath(prim_path) if prim_path else stage.GetPseudoRoot() + if not start_prim: + if prim_path: + logger.warning(f"No prim found at path '{prim_path}'. Returning from count for semantic labels.") + return {"missing_labels": 0} + + labels_counter = {"missing_labels": 0} + for prim in Usd.PrimRange(start_prim): + if prim.IsA(UsdGeom.Gprim): + labels_dict = get_labels(prim) + if not labels_dict: + labels_counter["missing_labels"] += 1 + else: + # Iterate through all labels from all instances on the prim + all_labels = [label for sublist in labels_dict.values() for label in sublist if label] + for label in all_labels: + labels_counter[label] = labels_counter.get(label, 0) + 1 + + return labels_counter diff --git a/source/isaaclab/isaaclab/sim/utils/stage.py b/source/isaaclab/isaaclab/sim/utils/stage.py new file mode 100644 index 000000000000..88c61744e7db --- /dev/null +++ b/source/isaaclab/isaaclab/sim/utils/stage.py @@ -0,0 +1,492 @@ +# 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 + +"""Utilities for operating on the USD stage.""" + +import builtins +import contextlib +import logging +import threading +from collections.abc import Callable, Generator + +import omni.kit.app +import omni.usd +from isaacsim.core.utils import stage as sim_stage +from pxr import Sdf, Usd, UsdUtils + +from isaaclab.utils.version import get_isaac_sim_version + +# import logger +logger = logging.getLogger(__name__) +_context = threading.local() # thread-local storage to handle nested contexts and concurrent access + +# _context is a singleton design in isaacsim and for that reason +# until we fully replace all modules that references the singleton(such as XformPrim, Prim ....), we have to point +# that singleton to this _context +sim_stage._context = _context # type: ignore + + +def create_new_stage() -> Usd.Stage: + """Create a new stage attached to the USD context. + + Returns: + Usd.Stage: The created USD stage. + + Raises: + RuntimeError: When failed to create a new stage. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.create_new_stage() + Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x7fba6c04f840:World7.usd'), + sessionLayer=Sdf.Find('anon:0x7fba6c01c5c0:World7-session.usda'), + pathResolverContext=) + """ + result = omni.usd.get_context().new_stage() + if result: + return omni.usd.get_context().get_stage() + else: + raise RuntimeError("Failed to create a new stage. Please check if the USD context is valid.") + + +def create_new_stage_in_memory() -> Usd.Stage: + """Creates a new stage in memory, if supported. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later. For backwards + compatibility, it falls back to creating a new stage attached to the USD context. + + Returns: + The new stage in memory. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.create_new_stage_in_memory() + Usd.Stage.Open(rootLayer=Sdf.Find('anon:0xf7b00e0:tmp.usda'), + sessionLayer=Sdf.Find('anon:0xf7cd2e0:tmp-session.usda'), + pathResolverContext=) + """ + if get_isaac_sim_version().major < 5: + logger.warning( + "Isaac Sim < 5.0 does not support creating a new stage in memory. Falling back to creating a new" + " stage attached to USD context." + ) + return create_new_stage() + else: + return Usd.Stage.CreateInMemory() + + +def is_current_stage_in_memory() -> bool: + """Checks if the current stage is in memory. + + This function compares the stage id of the current USD stage with the stage id of the USD context stage. + + Returns: + Whether the current stage is in memory. + """ + # grab current stage id + stage_id = get_current_stage_id() + + # grab context stage id + context_stage = omni.usd.get_context().get_stage() + with use_stage(context_stage): + context_stage_id = get_current_stage_id() + + # check if stage ids are the same + return stage_id != context_stage_id + + +def open_stage(usd_path: str) -> bool: + """Open the given usd file and replace currently opened stage. + + Args: + usd_path: The path to the USD file to open. + + Returns: + True if operation is successful, otherwise False. + + Raises: + ValueError: When input path is not a supported file type by USD. + """ + # check if USD file is supported + if not Usd.Stage.IsSupportedFile(usd_path): + raise ValueError(f"The USD file at path '{usd_path}' is not supported.") + + # get USD context + usd_context = omni.usd.get_context() + # disable save to recent files + usd_context.disable_save_to_recent_files() + # open stage + result = usd_context.open_stage(usd_path) + # enable save to recent files + usd_context.enable_save_to_recent_files() + # return result + return result + + +@contextlib.contextmanager +def use_stage(stage: Usd.Stage) -> Generator[None, None, None]: + """Context manager that sets a thread-local stage, if supported. + + This function binds the stage to the thread-local context for the duration of the context manager. + During the context manager, any call to :func:`get_current_stage` will return the stage specified + in the context manager. After the context manager is exited, the stage is restored to the default + stage attached to the USD context. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later. For backwards + compatibility, it falls back to a no-op context manager in Isaac Sim < 5.0. + + Args: + stage: The stage to set in the context. + + Returns: + A context manager that sets the stage in the context. + + Raises: + AssertionError: If the stage is not a USD stage instance. + + Example: + >>> from pxr import Usd + >>> import isaaclab.sim as sim_utils + >>> + >>> stage_in_memory = Usd.Stage.CreateInMemory() + >>> with sim_utils.use_stage(stage_in_memory): + ... # operate on the specified stage + ... pass + >>> # operate on the default stage attached to the USD context + """ + if get_isaac_sim_version().major < 5: + logger.warning("Isaac Sim < 5.0 does not support thread-local stage contexts. Skipping use_stage().") + yield # no-op + else: + # check stage + if not isinstance(stage, Usd.Stage): + raise TypeError(f"Expected a USD stage instance, got: {type(stage)}") + # store previous context value if it exists + previous_stage = getattr(_context, "stage", None) + # set new context value + try: + _context.stage = stage + yield + # remove context value or restore previous one if it exists + finally: + if previous_stage is None: + delattr(_context, "stage") + else: + _context.stage = previous_stage + + +def update_stage() -> None: + """Updates the current stage by triggering an application update cycle. + + This function triggers a single update cycle of the application interface, which + in turn updates the stage and all associated systems (rendering, physics, etc.). + This is necessary to ensure that changes made to the stage are properly processed + and reflected in the simulation. + + Note: + This function calls the application update interface rather than directly + updating the stage because the stage update is part of the broader + application update cycle that includes rendering, physics, and other systems. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.update_stage() + """ + # TODO: Why is this updating the simulation and not the stage? + omni.kit.app.get_app_interface().update() + + +def save_stage(usd_path: str, save_and_reload_in_place: bool = True) -> bool: + """Saves contents of the root layer of the current stage to the specified USD file. + + If the file already exists, it will be overwritten. + + Args: + usd_path: The file path to save the current stage to + save_and_reload_in_place: Whether to open the saved USD file in place. Defaults to True. + + Returns: + True if operation is successful, otherwise False. + + Raises: + ValueError: When input path is not a supported file type by USD. + RuntimeError: When layer creation or save operation fails. + """ + # check if USD file is supported + if not Usd.Stage.IsSupportedFile(usd_path): + raise ValueError(f"The USD file at path '{usd_path}' is not supported.") + + # create new layer + layer = Sdf.Layer.CreateNew(usd_path) + if layer is None: + raise RuntimeError(f"Failed to create new USD layer at path '{usd_path}'.") + + # get root layer + root_layer = get_current_stage().GetRootLayer() + # transfer content from root layer to new layer + layer.TransferContent(root_layer) + # resolve paths + omni.usd.resolve_paths(root_layer.identifier, layer.identifier) + # save layer + result = layer.Save() + if not result: + logger.error(f"Failed to save USD layer to path '{usd_path}'.") + + # if requested, open the saved USD file in place + if save_and_reload_in_place and result: + open_stage(usd_path) + + return result + + +def close_stage(callback_fn: Callable[[bool, str], None] | None = None) -> bool: + """Closes the current USD stage. + + .. note:: + + Once the stage is closed, it is necessary to open a new stage or create a + new one in order to work on it. + + Args: + callback_fn: A callback function to call while closing the stage. + The function should take two arguments: a boolean indicating whether the stage is closing + and a string indicating the error message if the stage closing fails. Defaults to None, + in which case the stage will be closed without a callback. + + Returns: + True if operation is successful, otherwise False. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.close_stage() + True + >>> + + Example with callback function: + >>> import isaaclab.sim as sim_utils + >>> + >>> def callback(*args, **kwargs): + ... print("callback:", args, kwargs) + >>> sim_utils.close_stage(callback) + True + >>> sim_utils.close_stage(callback) + callback: (False, 'Stage opening or closing already in progress!!') {} + False + """ + if callback_fn is None: + result = omni.usd.get_context().close_stage() + else: + result = omni.usd.get_context().close_stage_with_callback(callback_fn) + return result + + +def clear_stage(predicate: Callable[[Usd.Prim], bool] | None = None) -> None: + """Deletes all prims in the stage without populating the undo command buffer. + + The function will delete all prims in the stage that satisfy the predicate. If the predicate + is None, a default predicate will be used that deletes all prims. The default predicate deletes + all prims that are not the root prim, are not under the /Render namespace, have the ``no_delete`` + metadata, are not ancestral to any other prim, and are not hidden in the stage window. + + Args: + predicate: A user defined function that takes the USD prim as an argument and + returns a boolean indicating if the prim should be deleted. If the predicate is None, + a default predicate will be used that deletes all prims. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> # clear the whole stage + >>> sim_utils.clear_stage() + >>> + >>> # given the stage: /World/Cube, /World/Cube_01, /World/Cube_02. + >>> # Delete only the prims of type Cube + >>> predicate = lambda _prim: _prim.GetTypeName() == "Cube" + >>> sim_utils.clear_stage(predicate) # after the execution the stage will be /World + """ + # Note: Need to import this here to prevent circular dependencies. + from .prims import delete_prim + from .queries import get_all_matching_child_prims + + def _default_predicate(prim: Usd.Prim) -> bool: + """Check if the prim should be deleted.""" + prim_path = prim.GetPath().pathString + if prim_path == "/": + return False + if prim_path.startswith("/Render"): + return False + if prim.GetMetadata("no_delete"): + return False + if prim.GetMetadata("hide_in_stage_window"): + return False + if omni.usd.check_ancestral(prim): + return False + return True + + def _predicate_from_path(prim: Usd.Prim) -> bool: + if predicate is None: + return _default_predicate(prim) + return predicate(prim) + + # get all prims to delete + if predicate is None: + prims = get_all_matching_child_prims("/", _default_predicate) + else: + prims = get_all_matching_child_prims("/", _predicate_from_path) + # convert prims to prim paths + prim_paths_to_delete = [prim.GetPath().pathString for prim in prims] + # delete prims + delete_prim(prim_paths_to_delete) + + if builtins.ISAAC_LAUNCHED_FROM_TERMINAL is False: # type: ignore + omni.kit.app.get_app_interface().update() + + +def is_stage_loading() -> bool: + """Convenience function to see if any files are being loaded. + + Returns: + True if loading, False otherwise + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.is_stage_loading() + False + """ + context = omni.usd.get_context() + if context is None: + return False + else: + _, _, loading = context.get_stage_loading_status() + return loading > 0 + + +def get_current_stage(fabric: bool = False) -> Usd.Stage: + """Get the current open USD or Fabric stage + + Args: + fabric: True to get the fabric stage. False to get the USD stage. Defaults to False. + + Returns: + The USD or Fabric stage as specified by the input arg fabric. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.get_current_stage() + Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x7fba6c04f840:World7.usd'), + sessionLayer=Sdf.Find('anon:0x7fba6c01c5c0:World7-session.usda'), + pathResolverContext=) + """ + stage = getattr(_context, "stage", omni.usd.get_context().get_stage()) + + if fabric: + import usdrt + + # Get stage ID and attach to Fabric stage + stage_id = get_current_stage_id() + return usdrt.Usd.Stage.Attach(stage_id) + + return stage + + +def get_current_stage_id() -> int: + """Get the current open stage ID. + + Returns: + The current open stage id. + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> sim_utils.get_current_stage_id() + 1234567890 + """ + # get current stage + stage = get_current_stage() + # retrieve stage ID from stage cache + stage_cache = UsdUtils.StageCache.Get() + stage_id = stage_cache.GetId(stage).ToLongInt() + # if stage ID is not found, insert it into the stage cache + if stage_id < 0: + stage_id = stage_cache.Insert(stage).ToLongInt() + # return stage ID + return stage_id + + +def attach_stage_to_usd_context(attaching_early: bool = False): + """Attaches the current USD stage in memory to the USD context. + + This function should be called during or after scene is created and before stage is simulated or rendered. + If the stage is not in memory or rendering is not enabled, this function will return without attaching. + + .. versionadded:: 2.3.0 + This function is available in Isaac Sim 5.0 and later. For backwards + compatibility, it returns without attaching to the USD context. + + Args: + attaching_early: Whether to attach the stage to the usd context before stage is created. Defaults to False. + """ + + import carb + import omni.physx + import omni.usd + from isaacsim.core.simulation_manager import SimulationManager + + from isaaclab.sim.simulation_context import SimulationContext + + # if Isaac Sim version is less than 5.0, stage in memory is not supported + if get_isaac_sim_version().major < 5: + return + + # if stage is not in memory, we can return early + if not is_current_stage_in_memory(): + return + + # attach stage to physx + stage_id = get_current_stage_id() + physx_sim_interface = omni.physx.get_physx_simulation_interface() + physx_sim_interface.attach_stage(stage_id) + + # this carb flag is equivalent to if rendering is enabled + carb_setting = carb.settings.get_settings() # type: ignore + is_rendering_enabled = carb_setting.get("/physics/fabricUpdateTransformations") + + # if rendering is not enabled, we don't need to attach it + if not is_rendering_enabled: + return + + # early attach warning msg + if attaching_early: + logger.warning( + "Attaching stage in memory to USD context early to support an operation which" + " does not support stage in memory." + ) + + # skip this callback to avoid wiping the stage after attachment + SimulationContext.instance().skip_next_stage_open_callback() + + # disable stage open callback to avoid clearing callbacks + SimulationManager.enable_stage_open_callback(False) + + # enable physics fabric + SimulationContext.instance()._physics_context.enable_fabric(True) # type: ignore + + # attach stage to usd context + omni.usd.get_context().attach_stage_with_callback(stage_id) + + # attach stage to physx + physx_sim_interface = omni.physx.get_physx_simulation_interface() + physx_sim_interface.attach_stage(stage_id) + + # re-enable stage open callback + SimulationManager.enable_stage_open_callback(True) diff --git a/source/isaaclab/isaaclab/sim/utils/transforms.py b/source/isaaclab/isaaclab/sim/utils/transforms.py new file mode 100644 index 000000000000..d7ae57a16a6a --- /dev/null +++ b/source/isaaclab/isaaclab/sim/utils/transforms.py @@ -0,0 +1,453 @@ +# 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 + +"""Utilities for working with USD transform (xform) operations. + +This module provides utilities for manipulating USD transform operations (xform ops) on prims. +Transform operations in USD define how geometry is positioned, oriented, and scaled in 3D space. + +The utilities in this module help standardize transform stacks, clear operations, and manipulate +transforms in a consistent way across different USD assets. +""" + +from __future__ import annotations + +import logging + +from pxr import Gf, Sdf, Usd, UsdGeom + +# import logger +logger = logging.getLogger(__name__) + +_INVALID_XFORM_OPS = [ + "xformOp:rotateX", + "xformOp:rotateXZY", + "xformOp:rotateY", + "xformOp:rotateYXZ", + "xformOp:rotateYZX", + "xformOp:rotateZ", + "xformOp:rotateZYX", + "xformOp:rotateZXY", + "xformOp:rotateXYZ", + "xformOp:transform", +] +"""List of invalid xform ops that should be removed.""" + + +def standardize_xform_ops( + prim: Usd.Prim, + translation: tuple[float, ...] | None = None, + orientation: tuple[float, ...] | None = None, + scale: tuple[float, ...] | None = None, +) -> bool: + """Standardize the transform operation stack on a USD prim to a canonical form. + + This function converts a prim's transform stack to use the standard USD transform operation + order: [translate, orient, scale]. The function performs the following operations: + + 1. Validates that the prim is Xformable + 2. Captures the current local transform (translation, rotation, scale) + 3. Resolves and bakes unit scale conversions (xformOp:scale:unitsResolve) + 4. Creates or reuses standard transform operations (translate, orient, scale) + 5. Sets the transform operation order to [translate, orient, scale] + 6. Applies the preserved or user-specified transform values + + The entire modification is performed within an ``Sdf.ChangeBlock`` for optimal performance + when processing multiple prims. + + .. note:: + **Standard Transform Order:** The function enforces the USD best practice order: + ``xformOp:translate``, ``xformOp:orient``, ``xformOp:scale``. This order is + compatible with most USD tools and workflows, and uses quaternions for rotation + (avoiding gimbal lock issues). + + .. note:: + **Pose Preservation:** By default, the function preserves the prim's local transform + (relative to its parent). The world-space position of the prim remains unchanged + unless explicit ``translation``, ``orientation``, or ``scale`` values are provided. + + .. warning:: + **Animation Data Loss:** This function only preserves transform values at the default + time code (``Usd.TimeCode.Default()``). Any animation or time-sampled transform data + will be lost. Use this function during asset import or preparation, not on animated prims. + + .. warning:: + **Unit Scale Resolution:** If the prim has a ``xformOp:scale:unitsResolve`` attribute + (common in imported assets with unit mismatches), it will be baked into the scale + and removed. For example, a scale of (1, 1, 1) with unitsResolve of (100, 100, 100) + becomes a final scale of (100, 100, 100). + + Args: + prim: The USD prim to standardize. Must be a valid prim that supports the + UsdGeom.Xformable schema (e.g., Xform, Mesh, Cube, etc.). Material and + Shader prims are not Xformable and will return False. + translation: Optional translation vector (x, y, z) in local space. If provided, + overrides the prim's current translation. If None, preserves the current + local translation. Defaults to None. + orientation: Optional orientation quaternion (w, x, y, z) in local space. If provided, + overrides the prim's current orientation. If None, preserves the current + local orientation. Defaults to None. + scale: Optional scale vector (x, y, z). If provided, overrides the prim's current scale. + If None, preserves the current scale (after unit resolution) or uses (1, 1, 1) + if no scale exists. Defaults to None. + + Returns: + bool: True if the transform operations were successfully standardized. False if the + prim is not Xformable (e.g., Material, Shader prims). The function will log an + error message when returning False. + + Raises: + ValueError: If the prim is not valid (i.e., does not exist or is an invalid prim). + + Example: + >>> import isaaclab.sim as sim_utils + >>> + >>> # Standardize a prim with non-standard transform operations + >>> prim = stage.GetPrimAtPath("/World/ImportedAsset") + >>> result = sim_utils.standardize_xform_ops(prim) + >>> if result: + ... print("Transform stack standardized successfully") + >>> # The prim now uses: [translate, orient, scale] in that order + >>> + >>> # Standardize and set new transform values + >>> sim_utils.standardize_xform_ops( + ... prim, + ... translation=(1.0, 2.0, 3.0), + ... orientation=(1.0, 0.0, 0.0, 0.0), # identity rotation (w, x, y, z) + ... scale=(2.0, 2.0, 2.0), + ... ) + >>> + >>> # Batch processing for performance + >>> prims_to_standardize = [stage.GetPrimAtPath(p) for p in prim_paths] + >>> for prim in prims_to_standardize: + ... sim_utils.standardize_xform_ops(prim) # Each call uses Sdf.ChangeBlock + """ + # Validate prim + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim.GetPath()}' is not valid.") + + # Check if prim is an Xformable + if not prim.IsA(UsdGeom.Xformable): + logger.error( + f"Prim at path '{prim.GetPath().pathString}' is of type '{prim.GetTypeName()}', " + "which is not an Xformable. Transform operations will not be standardized. " + "This is expected for material, shader, and scope prims." + ) + return False + + # Create xformable interface + xformable = UsdGeom.Xformable(prim) + # Get current property names + prop_names = prim.GetPropertyNames() + + # Obtain current local transformations + tf = Gf.Transform(xformable.GetLocalTransformation()) + xform_pos = Gf.Vec3d(tf.GetTranslation()) + xform_quat = Gf.Quatd(tf.GetRotation().GetQuat()) + xform_scale = Gf.Vec3d(tf.GetScale()) + + if translation is not None: + xform_pos = Gf.Vec3d(*translation) + if orientation is not None: + xform_quat = Gf.Quatd(*orientation) + + # Handle scale resolution + if scale is not None: + # User provided scale + xform_scale = Gf.Vec3d(scale) + elif "xformOp:scale" in prop_names: + # Handle unit resolution for scale if present + # This occurs when assets are imported with different unit scales + # Reference: Omniverse Metrics Assembler + if "xformOp:scale:unitsResolve" in prop_names: + units_resolve = prim.GetAttribute("xformOp:scale:unitsResolve").Get() + for i in range(3): + xform_scale[i] = xform_scale[i] * units_resolve[i] + else: + # No scale exists, use default uniform scale + xform_scale = Gf.Vec3d(1.0, 1.0, 1.0) + + # Verify if xform stack is reset + has_reset = xformable.GetResetXformStack() + # Batch the operations + with Sdf.ChangeBlock(): + # Clear the existing transform operation order + for prop_name in prop_names: + if prop_name in _INVALID_XFORM_OPS: + prim.RemoveProperty(prop_name) + + # Remove unitsResolve attribute if present (already handled in scale resolution above) + if "xformOp:scale:unitsResolve" in prop_names: + prim.RemoveProperty("xformOp:scale:unitsResolve") + + # Set up or retrieve scale operation + xform_op_scale = UsdGeom.XformOp(prim.GetAttribute("xformOp:scale")) + if not xform_op_scale: + xform_op_scale = xformable.AddXformOp(UsdGeom.XformOp.TypeScale, UsdGeom.XformOp.PrecisionDouble, "") + + # Set up or retrieve translate operation + xform_op_translate = UsdGeom.XformOp(prim.GetAttribute("xformOp:translate")) + if not xform_op_translate: + xform_op_translate = xformable.AddXformOp( + UsdGeom.XformOp.TypeTranslate, UsdGeom.XformOp.PrecisionDouble, "" + ) + + # Set up or retrieve orient (quaternion rotation) operation + xform_op_orient = UsdGeom.XformOp(prim.GetAttribute("xformOp:orient")) + if not xform_op_orient: + xform_op_orient = xformable.AddXformOp(UsdGeom.XformOp.TypeOrient, UsdGeom.XformOp.PrecisionDouble, "") + + # Handle different floating point precisions + # Existing Xform operations might have floating or double precision. + # We need to cast the data to the correct type to avoid setting the wrong type. + xform_ops = [xform_op_translate, xform_op_orient, xform_op_scale] + xform_values = [xform_pos, xform_quat, xform_scale] + for xform_op, value in zip(xform_ops, xform_values): + # Get current value to determine precision type + current_value = xform_op.Get() + # Cast to existing type to preserve precision (float/double) + xform_op.Set(type(current_value)(value) if current_value is not None else value) + + # Set the transform operation order: translate -> orient -> scale + # This is the standard USD convention and ensures consistent behavior + xformable.SetXformOpOrder([xform_op_translate, xform_op_orient, xform_op_scale], has_reset) + + return True + + +def validate_standard_xform_ops(prim: Usd.Prim) -> bool: + """Validate if the transform operations on a prim are standardized. + + This function checks if the transform operations on a prim are standardized to the canonical form: + [translate, orient, scale]. + + Args: + prim: The USD prim to validate. + """ + # check if prim is valid + if not prim.IsValid(): + logger.error(f"Prim at path '{prim.GetPath().pathString}' is not valid.") + return False + # check if prim is an xformable + if not prim.IsA(UsdGeom.Xformable): + logger.error(f"Prim at path '{prim.GetPath().pathString}' is not an xformable.") + return False + # get the xformable interface + xformable = UsdGeom.Xformable(prim) + # get the xform operation order + xform_op_order = xformable.GetOrderedXformOps() + xform_op_order = [op.GetOpName() for op in xform_op_order] + # check if the xform operation order is the canonical form + if xform_op_order != ["xformOp:translate", "xformOp:orient", "xformOp:scale"]: + msg = f"Xform operation order for prim at path '{prim.GetPath().pathString}' is not the canonical form." + msg += f" Received order: {xform_op_order}" + msg += " Expected order: ['xformOp:translate', 'xformOp:orient', 'xformOp:scale']" + logger.error(msg) + return False + return True + + +def resolve_prim_pose( + prim: Usd.Prim, ref_prim: Usd.Prim | None = None +) -> tuple[tuple[float, float, float], tuple[float, float, float, float]]: + """Resolve the pose of a prim with respect to another prim. + + Note: + This function ignores scale and skew by orthonormalizing the transformation + matrix at the final step. However, if any ancestor prim in the hierarchy + has non-uniform scale, that scale will still affect the resulting position + and orientation of the prim (because it's baked into the transform before + scale removal). + + In other words: scale **is not removed hierarchically**. If you need + completely scale-free poses, you must walk the transform chain and strip + scale at each level. Please open an issue if you need this functionality. + + Args: + prim: The USD prim to resolve the pose for. + ref_prim: The USD prim to compute the pose with respect to. + Defaults to None, in which case the world frame is used. + + Returns: + A tuple containing the position (as a 3D vector) and the quaternion orientation + in the (w, x, y, z) format. + + Raises: + ValueError: If the prim or ref prim is not valid. + + Example: + >>> import isaaclab.sim as sim_utils + >>> from pxr import Usd, UsdGeom + >>> + >>> # Get prim + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/ImportedAsset") + >>> + >>> # Resolve pose + >>> pos, quat = sim_utils.resolve_prim_pose(prim) + >>> print(f"Position: {pos}") + >>> print(f"Orientation: {quat}") + >>> + >>> # Resolve pose with respect to another prim + >>> ref_prim = stage.GetPrimAtPath("/World/Reference") + >>> pos, quat = sim_utils.resolve_prim_pose(prim, ref_prim) + >>> print(f"Position: {pos}") + >>> print(f"Orientation: {quat}") + """ + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim.GetPath().pathString}' is not valid.") + # get prim xform + xform = UsdGeom.Xformable(prim) + prim_tf = xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) + # sanitize quaternion + # this is needed, otherwise the quaternion might be non-normalized + prim_tf.Orthonormalize() + + if ref_prim is not None: + # if reference prim is the root, we can skip the computation + if ref_prim.GetPath() != Sdf.Path.absoluteRootPath: + # get ref prim xform + ref_xform = UsdGeom.Xformable(ref_prim) + ref_tf = ref_xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) + # make sure ref tf is orthonormal + ref_tf.Orthonormalize() + # compute relative transform to get prim in ref frame + prim_tf = prim_tf * ref_tf.GetInverse() + + # extract position and orientation + prim_pos = [*prim_tf.ExtractTranslation()] + prim_quat = [prim_tf.ExtractRotationQuat().real, *prim_tf.ExtractRotationQuat().imaginary] + return tuple(prim_pos), tuple(prim_quat) + + +def resolve_prim_scale(prim: Usd.Prim) -> tuple[float, float, float]: + """Resolve the scale of a prim in the world frame. + + At an attribute level, a USD prim's scale is a scaling transformation applied to the prim with + respect to its parent prim. This function resolves the scale of the prim in the world frame, + by computing the local to world transform of the prim. This is equivalent to traversing up + the prim hierarchy and accounting for the rotations and scales of the prims. + + For instance, if a prim has a scale of (1, 2, 3) and it is a child of a prim with a scale of (4, 5, 6), + then the scale of the prim in the world frame is (4, 10, 18). + + Args: + prim: The USD prim to resolve the scale for. + + Returns: + The scale of the prim in the x, y, and z directions in the world frame. + + Raises: + ValueError: If the prim is not valid. + + Example: + >>> import isaaclab.sim as sim_utils + >>> from pxr import Usd, UsdGeom + >>> + >>> # Get prim + >>> stage = sim_utils.get_current_stage() + >>> prim = stage.GetPrimAtPath("/World/ImportedAsset") + >>> + >>> # Resolve scale + >>> scale = sim_utils.resolve_prim_scale(prim) + >>> print(f"Scale: {scale}") + """ + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim.GetPath().pathString}' is not valid.") + # compute local to world transform + xform = UsdGeom.Xformable(prim) + world_transform = xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) + # extract scale + return tuple([*(v.GetLength() for v in world_transform.ExtractRotationMatrix())]) + + +def convert_world_pose_to_local( + position: tuple[float, ...], + orientation: tuple[float, ...] | None, + ref_prim: Usd.Prim, +) -> tuple[tuple[float, float, float], tuple[float, float, float, float] | None]: + """Convert a world-space pose to local-space pose relative to a reference prim. + + This function takes a position and orientation in world space and converts them to local space + relative to the given reference prim. This is useful when creating or positioning prims where you + know the desired world position but need to set local transform attributes relative to another prim. + + The conversion uses the standard USD transformation math: + ``local_transform = world_transform * inverse(ref_world_transform)`` + + .. note:: + If the reference prim is the root prim ("/"), the position and orientation are returned + unchanged, as they are already effectively in local/world space. + + Args: + position: The world-space position as (x, y, z). + orientation: The world-space orientation as quaternion (w, x, y, z). If None, only position is converted + and None is returned for orientation. + ref_prim: The reference USD prim to compute the local transform relative to. If this is + the root prim ("/"), the world pose is returned unchanged. + + Returns: + A tuple of (local_translation, local_orientation) where: + + - local_translation is a tuple of (x, y, z) in local space relative to ref_prim + - local_orientation is a tuple of (w, x, y, z) in local space relative to ref_prim, + or None if no orientation was provided + + Raises: + ValueError: If the reference prim is not a valid USD prim. + + Example: + >>> import isaaclab.sim as sim_utils + >>> from pxr import Usd, UsdGeom + >>> + >>> # Get reference prim + >>> stage = sim_utils.get_current_stage() + >>> ref_prim = stage.GetPrimAtPath("/World/Reference") + >>> + >>> # Convert world pose to local (relative to ref_prim) + >>> world_pos = (10.0, 5.0, 0.0) + >>> world_quat = (1.0, 0.0, 0.0, 0.0) # identity rotation + >>> local_pos, local_quat = sim_utils.convert_world_pose_to_local(world_pos, world_quat, ref_prim) + >>> print(f"Local position: {local_pos}") + >>> print(f"Local orientation: {local_quat}") + """ + # Check if prim is valid + if not ref_prim.IsValid(): + raise ValueError(f"Reference prim at path '{ref_prim.GetPath().pathString}' is not valid.") + + # If reference prim is the root, return world pose as-is + if ref_prim.GetPath() == Sdf.Path.absoluteRootPath: + return position, orientation # type: ignore + + # Check if reference prim is a valid xformable + ref_xformable = UsdGeom.Xformable(ref_prim) + # Get reference prim's world transform + ref_world_tf = ref_xformable.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) + + # Create world transform for the desired position and orientation + desired_world_tf = Gf.Matrix4d() + desired_world_tf.SetTranslateOnly(Gf.Vec3d(*position)) + + if orientation is not None: + # Set rotation from quaternion (w, x, y, z) + quat = Gf.Quatd(*orientation) + desired_world_tf.SetRotateOnly(quat) + + # Convert world transform to local: local = world * inv(ref_world) + ref_world_tf_inv = ref_world_tf.GetInverse() + local_tf = desired_world_tf * ref_world_tf_inv + + # Extract local translation and orientation + local_transform = Gf.Transform(local_tf) + local_translation = tuple(local_transform.GetTranslation()) + + local_orientation = None + if orientation is not None: + quat_result = local_transform.GetRotation().GetQuat() + local_orientation = (quat_result.GetReal(), *quat_result.GetImaginary()) + + return local_translation, local_orientation diff --git a/source/isaaclab/isaaclab/sim/views/__init__.py b/source/isaaclab/isaaclab/sim/views/__init__.py new file mode 100644 index 000000000000..eb5bea7690cb --- /dev/null +++ b/source/isaaclab/isaaclab/sim/views/__init__.py @@ -0,0 +1,8 @@ +# 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 + +"""Views for manipulating USD prims.""" + +from .xform_prim_view import XformPrimView diff --git a/source/isaaclab/isaaclab/sim/views/xform_prim_view.py b/source/isaaclab/isaaclab/sim/views/xform_prim_view.py new file mode 100644 index 000000000000..38a786117bcf --- /dev/null +++ b/source/isaaclab/isaaclab/sim/views/xform_prim_view.py @@ -0,0 +1,1114 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import logging +from collections.abc import Sequence + +import numpy as np +import torch +import warp as wp + +import carb +from pxr import Gf, Sdf, Usd, UsdGeom, Vt + +import isaaclab.sim as sim_utils +import isaaclab.utils.math as math_utils +from isaaclab.utils.warp import fabric as fabric_utils + +logger = logging.getLogger(__name__) + + +class XformPrimView: + """Optimized batched interface for reading and writing transforms of multiple USD prims. + + This class provides efficient batch operations for getting and setting poses (position and orientation) + of multiple prims at once using torch tensors. It is designed for scenarios where you need to manipulate + many prims simultaneously, such as in multi-agent simulations or large-scale procedural generation. + + The class supports both world-space and local-space pose operations: + + - **World poses**: Positions and orientations in the global world frame + - **Local poses**: Positions and orientations relative to each prim's parent + + When Fabric is enabled, the class leverages NVIDIA's Fabric API for GPU-accelerated batch operations: + + - Uses `omni:fabric:worldMatrix` and `omni:fabric:localMatrix` attributes for all Boundable prims + - Performs batch matrix decomposition/composition using Warp kernels on GPU + - Achieves performance comparable to Isaac Sim's XFormPrim implementation + - Works for both physics-enabled and non-physics prims (cameras, meshes, etc.). + Note: renderers typically consume USD-authored camera transforms. + + .. warning:: + **Fabric requires CUDA**: Fabric is only supported with on CUDA devices. + Warp's CPU backend for fabric-array writes has known issues, so attempting to use + Fabric with CPU device (``device="cpu"``) will raise a ValueError at initialization. + + .. note:: + **Fabric Support:** + + When Fabric is enabled, this view ensures prims have the required Fabric hierarchy + attributes (``omni:fabric:localMatrix`` and ``omni:fabric:worldMatrix``). On first Fabric + read, USD-authored transforms initialize Fabric state. Fabric writes can optionally + be mirrored back to USD via :attr:`sync_usd_on_fabric_write`. + + For more information, see the `Fabric Hierarchy documentation`_. + + .. _Fabric Hierarchy documentation: https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/fabric_hierarchy.html + + .. note:: + **Performance Considerations:** + + * Tensor operations are performed on the specified device (CPU/CUDA) + * USD write operations use ``Sdf.ChangeBlock`` for batched updates + * Fabric operations use GPU-accelerated Warp kernels for maximum performance + * For maximum performance, minimize get/set operations within tight loops + + .. note:: + **Transform Requirements:** + + All prims in the view must be Xformable and have standardized transform operations: + ``[translate, orient, scale]``. Non-standard prims will raise a ValueError during + initialization if :attr:`validate_xform_ops` is True. Please use the function + :func:`isaaclab.sim.utils.standardize_xform_ops` to prepare prims before using this view. + + .. warning:: + This class operates at the USD default time code. Any animation or time-sampled data + will not be affected by write operations. For animated transforms, you need to handle + time-sampled keyframes separately. + """ + + def __init__( + self, + prim_path: str, + device: str = "cpu", + validate_xform_ops: bool = True, + sync_usd_on_fabric_write: bool = False, + stage: Usd.Stage | None = None, + ): + """Initialize the view with matching prims. + + This method searches the USD stage for all prims matching the provided path pattern, + validates that they are Xformable with standard transform operations, and stores + references for efficient batch operations. + + We generally recommend to validate the xform operations, as it ensures that the prims are in a consistent state + and have the standard transform operations (translate, orient, scale in that order). + However, if you are sure that the prims are in a consistent state, you can set this to False to improve + performance. This can save around 45-50% of the time taken to initialize the view. + + Args: + prim_path: USD prim path pattern to match prims. Supports wildcards (``*``) and + regex patterns (e.g., ``"/World/Env_.*/Robot"``). See + :func:`isaaclab.sim.utils.find_matching_prims` for pattern syntax. + device: Device to place the tensors on. Can be ``"cpu"`` or CUDA devices like + ``"cuda:0"``. Defaults to ``"cpu"``. + validate_xform_ops: Whether to validate that the prims have standard xform operations. + Defaults to True. + sync_usd_on_fabric_write: Whether to mirror Fabric transform writes back to USD. + When True, transform updates are synchronized to USD so that USD data readers (e.g., rendering + cameras) can observe these changes. Defaults to False for better performance. + stage: USD stage to search for prims. Defaults to None, in which case the current active stage + from the simulation context is used. + + Raises: + ValueError: If any matched prim is not Xformable or doesn't have standardized + transform operations (translate, orient, scale in that order). + """ + # Store configuration + self._prim_path = prim_path + self._device = device + + # Find and validate matching prims + stage = sim_utils.get_current_stage() if stage is None else stage + self._prims: list[Usd.Prim] = sim_utils.find_matching_prims(prim_path, stage=stage) + + # Validate all prims have standard xform operations + if validate_xform_ops: + for prim in self._prims: + if not sim_utils.validate_standard_xform_ops(prim): + raise ValueError( + f"Prim at path '{prim.GetPath().pathString}' is not a xformable prim with standard transform" + f" operations [translate, orient, scale]. Received type: '{prim.GetTypeName()}'." + " Use sim_utils.standardize_xform_ops() to prepare the prim." + ) + + # Determine if Fabric is supported on the device + self._use_fabric = carb.settings.get_settings().get("/physics/fabricEnabled") + logger.debug(f"Using Fabric for the XFormPrimView over '{self._prim_path}' on device '{self._device}'.") + + # Check for unsupported Fabric + CPU combination + if self._use_fabric and self._device == "cpu": + logger.warning( + "Fabric mode with Warp fabric-array operations is not supported on CPU devices. " + "While Fabric itself can run on both CPU and GPU, our batch Warp kernels for " + "fabric-array operations require CUDA and are not reliable on the CPU backend. " + "To ensure stability, Fabric is being disabled and execution will fall back " + "to standard USD operations on the CPU. This may impact performance." + ) + self._use_fabric = False + + # Create indices buffer + # Since we iterate over the indices, we need to use range instead of torch tensor + self._ALL_INDICES = list(range(len(self._prims))) + + # Some prims (e.g., Cameras) require USD-authored transforms for rendering. + # When enabled, mirror Fabric pose writes to USD for those prims. + self._sync_usd_on_fabric_write = sync_usd_on_fabric_write + + # Fabric batch infrastructure (initialized lazily on first use) + self._fabric_initialized = False + self._fabric_usd_sync_done = False + self._fabric_selection = None + self._fabric_to_view: wp.array | None = None + self._view_to_fabric: wp.array | None = None + self._default_view_indices: wp.array | None = None + self._fabric_hierarchy = None + # Create a valid USD attribute name: namespace:name + # Use "isaaclab" namespace to identify our custom attributes + self._view_index_attr = f"isaaclab:view_index:{abs(hash(self))}" + + """ + Properties. + """ + + @property + def count(self) -> int: + """Number of prims in this view.""" + return len(self._prims) + + @property + def device(self) -> str: + """Device where tensors are allocated (cpu or cuda).""" + return self._device + + @property + def prims(self) -> list[Usd.Prim]: + """List of USD prims being managed by this view.""" + return self._prims + + @property + def prim_paths(self) -> list[str]: + """List of prim paths (as strings) for all prims being managed by this view. + + This property converts each prim to its path string representation. The conversion is + performed lazily on first access and cached for subsequent accesses. + + Note: + For most use cases, prefer using :attr:`prims` directly as it provides direct access + to the USD prim objects without the conversion overhead. This property is mainly useful + for logging, debugging, or when string paths are explicitly required. + """ + # we cache it the first time it is accessed. + # we don't compute it in constructor because it is expensive and we don't need it most of the time. + # users should usually deal with prims directly as they typically need to access the prims directly. + if not hasattr(self, "_prim_paths"): + self._prim_paths = [prim.GetPath().pathString for prim in self._prims] + return self._prim_paths + + """ + Operations - Setters. + """ + + def set_world_poses( + self, + positions: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + indices: Sequence[int] | None = None, + ): + """Set world-space poses for prims in the view. + + This method sets the position and/or orientation of each prim in world space. + + - When Fabric is enabled, the function writes directly to Fabric's ``omni:fabric:worldMatrix`` + attribute using GPU-accelerated batch operations. + - When Fabric is disabled, the function converts to local space and writes to USD's ``xformOp:translate`` + and ``xformOp:orient`` attributes. + + Args: + positions: World-space positions as a tensor of shape (M, 3) where M is the number of prims + to set (either all prims if indices is None, or the number of indices provided). + Defaults to None, in which case positions are not modified. + orientations: World-space orientations as quaternions (w, x, y, z) with shape (M, 4). + Defaults to None, in which case orientations are not modified. + indices: Indices of prims to set poses for. Defaults to None, in which case poses are set + for all prims in the view. + + Raises: + ValueError: If positions shape is not (M, 3) or orientations shape is not (M, 4). + ValueError: If the number of poses doesn't match the number of indices provided. + """ + if self._use_fabric: + self._set_world_poses_fabric(positions, orientations, indices) + else: + self._set_world_poses_usd(positions, orientations, indices) + + def set_local_poses( + self, + translations: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + indices: Sequence[int] | None = None, + ): + """Set local-space poses for prims in the view. + + This method sets the position and/or orientation of each prim in local space (relative to + their parent prims). + + The function writes directly to USD's ``xformOp:translate`` and ``xformOp:orient`` attributes. + + Note: + Even in Fabric mode, local pose operations use USD. This behavior is based on Isaac Sim's design + where Fabric is only used for world pose operations. + + Rationale: + - Local pose writes need correct parent-child hierarchy relationships + - USD maintains these relationships correctly and efficiently + - Fabric is optimized for world pose operations, not local hierarchies + + Args: + translations: Local-space translations as a tensor of shape (M, 3) where M is the number of prims + to set (either all prims if indices is None, or the number of indices provided). + Defaults to None, in which case translations are not modified. + orientations: Local-space orientations as quaternions (w, x, y, z) with shape (M, 4). + Defaults to None, in which case orientations are not modified. + indices: Indices of prims to set poses for. Defaults to None, in which case poses are set + for all prims in the view. + + Raises: + ValueError: If translations shape is not (M, 3) or orientations shape is not (M, 4). + ValueError: If the number of poses doesn't match the number of indices provided. + """ + if self._use_fabric: + self._set_local_poses_fabric(translations, orientations, indices) + else: + self._set_local_poses_usd(translations, orientations, indices) + + def set_scales(self, scales: torch.Tensor, indices: Sequence[int] | None = None): + """Set scales for prims in the view. + + This method sets the scale of each prim in the view. + + - When Fabric is enabled, the function updates scales in Fabric matrices using GPU-accelerated batch operations. + - When Fabric is disabled, the function writes to USD's ``xformOp:scale`` attributes. + + Args: + scales: Scales as a tensor of shape (M, 3) where M is the number of prims + to set (either all prims if indices is None, or the number of indices provided). + indices: Indices of prims to set scales for. Defaults to None, in which case scales are set + for all prims in the view. + + Raises: + ValueError: If scales shape is not (M, 3). + """ + if self._use_fabric: + self._set_scales_fabric(scales, indices) + else: + self._set_scales_usd(scales, indices) + + def set_visibility(self, visibility: torch.Tensor, indices: Sequence[int] | None = None): + """Set visibility for prims in the view. + + This method sets the visibility of each prim in the view. + + Args: + visibility: Visibility as a boolean tensor of shape (M,) where M is the + number of prims to set (either all prims if indices is None, or the number of indices provided). + indices: Indices of prims to set visibility for. Defaults to None, in which case visibility is set + for all prims in the view. + + Raises: + ValueError: If visibility shape is not (M,). + """ + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Validate inputs + if visibility.shape != (len(indices_list),): + raise ValueError(f"Expected visibility shape ({len(indices_list)},), got {visibility.shape}.") + + # Set visibility for each prim + with Sdf.ChangeBlock(): + for idx, prim_idx in enumerate(indices_list): + # Convert prim to imageable + imageable = UsdGeom.Imageable(self._prims[prim_idx]) + # Set visibility + if visibility[idx]: + imageable.MakeVisible() + else: + imageable.MakeInvisible() + + """ + Operations - Getters. + """ + + def get_world_poses(self, indices: Sequence[int] | None = None) -> tuple[torch.Tensor, torch.Tensor]: + """Get world-space poses for prims in the view. + + This method retrieves the position and orientation of each prim in world space by computing + the full transform hierarchy from the prim to the world root. + + - When Fabric is enabled, the function uses Fabric batch operations with Warp kernels. + - When Fabric is disabled, the function uses USD XformCache. + + Note: + Scale and skew are ignored. The returned poses contain only translation and rotation. + + Args: + indices: Indices of prims to get poses for. Defaults to None, in which case poses are retrieved + for all prims in the view. + + Returns: + A tuple of (positions, orientations) where: + + - positions: Torch tensor of shape (M, 3) containing world-space positions (x, y, z), + where M is the number of prims queried. + - orientations: Torch tensor of shape (M, 4) containing world-space quaternions (w, x, y, z) + """ + if self._use_fabric: + return self._get_world_poses_fabric(indices) + else: + return self._get_world_poses_usd(indices) + + def get_local_poses(self, indices: Sequence[int] | None = None) -> tuple[torch.Tensor, torch.Tensor]: + """Get local-space poses for prims in the view. + + This method retrieves the position and orientation of each prim in local space (relative to + their parent prims). It reads directly from USD's ``xformOp:translate`` and ``xformOp:orient`` attributes. + + Note: + Even in Fabric mode, local pose operations use USD. This behavior is based on Isaac Sim's design + where Fabric is only used for world pose operations. + + Rationale: + - Local pose reads need correct parent-child hierarchy relationships + - USD maintains these relationships correctly and efficiently + - Fabric is optimized for world pose operations, not local hierarchies + + Note: + Scale is ignored. The returned poses contain only translation and rotation. + + Args: + indices: Indices of prims to get poses for. Defaults to None, in which case poses are retrieved + for all prims in the view. + + Returns: + A tuple of (translations, orientations) where: + + - translations: Torch tensor of shape (M, 3) containing local-space translations (x, y, z), + where M is the number of prims queried. + - orientations: Torch tensor of shape (M, 4) containing local-space quaternions (w, x, y, z) + """ + if self._use_fabric: + return self._get_local_poses_fabric(indices) + else: + return self._get_local_poses_usd(indices) + + def get_scales(self, indices: Sequence[int] | None = None) -> torch.Tensor: + """Get scales for prims in the view. + + This method retrieves the scale of each prim in the view. + + - When Fabric is enabled, the function extracts scales from Fabric matrices using batch operations with + Warp kernels. + - When Fabric is disabled, the function reads from USD's ``xformOp:scale`` attributes. + + Args: + indices: Indices of prims to get scales for. Defaults to None, in which case scales are retrieved + for all prims in the view. + + Returns: + A tensor of shape (M, 3) containing the scales of each prim, where M is the number of prims queried. + """ + if self._use_fabric: + return self._get_scales_fabric(indices) + else: + return self._get_scales_usd(indices) + + def get_visibility(self, indices: Sequence[int] | None = None) -> torch.Tensor: + """Get visibility for prims in the view. + + This method retrieves the visibility of each prim in the view. + + Args: + indices: Indices of prims to get visibility for. Defaults to None, in which case visibility is retrieved + for all prims in the view. + + Returns: + A tensor of shape (M,) containing the visibility of each prim, where M is the number of prims queried. + The tensor is of type bool. + """ + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + # Convert to list if it is a tensor array + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Create buffers + visibility = torch.zeros(len(indices_list), dtype=torch.bool, device=self._device) + + for idx, prim_idx in enumerate(indices_list): + # Get prim + imageable = UsdGeom.Imageable(self._prims[prim_idx]) + # Get visibility + visibility[idx] = imageable.ComputeVisibility() != UsdGeom.Tokens.invisible + + return visibility + + """ + Internal Functions - USD. + """ + + def _set_world_poses_usd( + self, + positions: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + indices: Sequence[int] | None = None, + ): + """Set world poses to USD.""" + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + # Convert to list if it is a tensor array + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Validate inputs + if positions is not None: + if positions.shape != (len(indices_list), 3): + raise ValueError( + f"Expected positions shape ({len(indices_list)}, 3), got {positions.shape}. " + "Number of positions must match the number of prims in the view." + ) + positions_array = Vt.Vec3dArray.FromNumpy(positions.cpu().numpy()) + else: + positions_array = None + if orientations is not None: + if orientations.shape != (len(indices_list), 4): + raise ValueError( + f"Expected orientations shape ({len(indices_list)}, 4), got {orientations.shape}. " + "Number of orientations must match the number of prims in the view." + ) + # Vt expects quaternions in xyzw order + orientations_array = Vt.QuatdArray.FromNumpy(math_utils.convert_quat(orientations, to="xyzw").cpu().numpy()) + else: + orientations_array = None + + # Create xform cache instance + xform_cache = UsdGeom.XformCache(Usd.TimeCode.Default()) + + # Set poses for each prim + # We use Sdf.ChangeBlock to minimize notification overhead. + with Sdf.ChangeBlock(): + for idx, prim_idx in enumerate(indices_list): + # Get prim + prim = self._prims[prim_idx] + # Get parent prim for local space conversion + parent_prim = prim.GetParent() + + # Determine what to set + world_pos = positions_array[idx] if positions_array is not None else None + world_quat = orientations_array[idx] if orientations_array is not None else None + + # Convert world pose to local if we have a valid parent + # Note: We don't use :func:`isaaclab.sim.utils.transforms.convert_world_pose_to_local` + # here since it isn't optimized for batch operations. + if parent_prim.IsValid() and parent_prim.GetPath() != Sdf.Path.absoluteRootPath: + # Get current world pose if we're only setting one component + if positions_array is None or orientations_array is None: + # get prim xform + prim_tf = xform_cache.GetLocalToWorldTransform(prim) + # sanitize quaternion + # this is needed, otherwise the quaternion might be non-normalized + prim_tf.Orthonormalize() + # populate desired world transform + if world_pos is not None: + prim_tf.SetTranslateOnly(world_pos) + if world_quat is not None: + prim_tf.SetRotateOnly(world_quat) + else: + # Both position and orientation are provided, create new transform + prim_tf = Gf.Matrix4d() + prim_tf.SetTranslateOnly(world_pos) + prim_tf.SetRotateOnly(world_quat) + + # Convert to local space + parent_world_tf = xform_cache.GetLocalToWorldTransform(parent_prim) + local_tf = prim_tf * parent_world_tf.GetInverse() + local_pos = local_tf.ExtractTranslation() + local_quat = local_tf.ExtractRotationQuat() + else: + # No parent or parent is root, world == local + local_pos = world_pos + local_quat = world_quat + + # Get or create the standard transform operations + if local_pos is not None: + prim.GetAttribute("xformOp:translate").Set(local_pos) + if local_quat is not None: + prim.GetAttribute("xformOp:orient").Set(local_quat) + + def _set_local_poses_usd( + self, + translations: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + indices: Sequence[int] | None = None, + ): + """Set local poses to USD.""" + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Validate inputs + if translations is not None: + if translations.shape != (len(indices_list), 3): + raise ValueError(f"Expected translations shape ({len(indices_list)}, 3), got {translations.shape}.") + translations_array = Vt.Vec3dArray.FromNumpy(translations.cpu().numpy()) + else: + translations_array = None + if orientations is not None: + if orientations.shape != (len(indices_list), 4): + raise ValueError(f"Expected orientations shape ({len(indices_list)}, 4), got {orientations.shape}.") + orientations_array = Vt.QuatdArray.FromNumpy(math_utils.convert_quat(orientations, to="xyzw").cpu().numpy()) + else: + orientations_array = None + + # Set local poses + with Sdf.ChangeBlock(): + for idx, prim_idx in enumerate(indices_list): + prim = self._prims[prim_idx] + if translations_array is not None: + prim.GetAttribute("xformOp:translate").Set(translations_array[idx]) + if orientations_array is not None: + prim.GetAttribute("xformOp:orient").Set(orientations_array[idx]) + + def _set_scales_usd(self, scales: torch.Tensor, indices: Sequence[int] | None = None): + """Set scales to USD.""" + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Validate inputs + if scales.shape != (len(indices_list), 3): + raise ValueError(f"Expected scales shape ({len(indices_list)}, 3), got {scales.shape}.") + + scales_array = Vt.Vec3dArray.FromNumpy(scales.cpu().numpy()) + # Set scales for each prim + with Sdf.ChangeBlock(): + for idx, prim_idx in enumerate(indices_list): + prim = self._prims[prim_idx] + prim.GetAttribute("xformOp:scale").Set(scales_array[idx]) + + def _get_world_poses_usd(self, indices: Sequence[int] | None = None) -> tuple[torch.Tensor, torch.Tensor]: + """Get world poses from USD.""" + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + # Convert to list if it is a tensor array + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Create buffers + positions = Vt.Vec3dArray(len(indices_list)) + orientations = Vt.QuatdArray(len(indices_list)) + # Create xform cache instance + xform_cache = UsdGeom.XformCache(Usd.TimeCode.Default()) + + # Note: We don't use :func:`isaaclab.sim.utils.transforms.resolve_prim_pose` + # here since it isn't optimized for batch operations. + for idx, prim_idx in enumerate(indices_list): + # Get prim + prim = self._prims[prim_idx] + # get prim xform + prim_tf = xform_cache.GetLocalToWorldTransform(prim) + # sanitize quaternion + # this is needed, otherwise the quaternion might be non-normalized + prim_tf.Orthonormalize() + # extract position and orientation + positions[idx] = prim_tf.ExtractTranslation() + orientations[idx] = prim_tf.ExtractRotationQuat() + + # move to torch tensors + positions = torch.tensor(np.array(positions), dtype=torch.float32, device=self._device) + orientations = torch.tensor(np.array(orientations), dtype=torch.float32, device=self._device) + # underlying data is in xyzw order, convert to wxyz order + orientations = math_utils.convert_quat(orientations, to="wxyz") + + return positions, orientations # type: ignore + + def _get_local_poses_usd(self, indices: Sequence[int] | None = None) -> tuple[torch.Tensor, torch.Tensor]: + """Get local poses from USD.""" + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Create buffers + translations = Vt.Vec3dArray(len(indices_list)) + orientations = Vt.QuatdArray(len(indices_list)) + + # Create a fresh XformCache to avoid stale cached values + xform_cache = UsdGeom.XformCache(Usd.TimeCode.Default()) + + for idx, prim_idx in enumerate(indices_list): + prim = self._prims[prim_idx] + prim_tf = xform_cache.GetLocalTransformation(prim)[0] + prim_tf.Orthonormalize() + translations[idx] = prim_tf.ExtractTranslation() + orientations[idx] = prim_tf.ExtractRotationQuat() + + translations = torch.tensor(np.array(translations), dtype=torch.float32, device=self._device) + orientations = torch.tensor(np.array(orientations), dtype=torch.float32, device=self._device) + orientations = math_utils.convert_quat(orientations, to="wxyz") + + return translations, orientations # type: ignore + + def _get_scales_usd(self, indices: Sequence[int] | None = None) -> torch.Tensor: + """Get scales from USD.""" + # Resolve indices + if indices is None or indices == slice(None): + indices_list = self._ALL_INDICES + else: + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + + # Create buffers + scales = Vt.Vec3dArray(len(indices_list)) + + for idx, prim_idx in enumerate(indices_list): + prim = self._prims[prim_idx] + scales[idx] = prim.GetAttribute("xformOp:scale").Get() + + # Convert to tensor + return torch.tensor(np.array(scales), dtype=torch.float32, device=self._device) + + """ + Internal Functions - Fabric. + """ + + def _set_world_poses_fabric( + self, + positions: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + indices: Sequence[int] | None = None, + ): + """Set world poses using Fabric GPU batch operations. + + Writes directly to Fabric's ``omni:fabric:worldMatrix`` attribute using Warp kernels. + Changes are propagated through Fabric's hierarchy system but remain GPU-resident. + + For workflows mixing Fabric world pose writes with USD local pose queries, note + that local poses read from USD's xformOp:* attributes, which may not immediately + reflect Fabric changes. For best performance and consistency, use Fabric methods + exclusively (get_world_poses/set_world_poses with Fabric enabled). + """ + # Lazy initialization + if not self._fabric_initialized: + self._initialize_fabric() + + # Resolve indices (treat slice(None) as None for consistency with USD path) + indices_wp = self._resolve_indices_wp(indices) + + count = indices_wp.shape[0] + + # Convert torch to warp (if provided), use dummy arrays for None to avoid Warp kernel issues + if positions is not None: + positions_wp = wp.from_torch(positions) + else: + positions_wp = wp.zeros((0, 3), dtype=wp.float32).to(self._device) + + if orientations is not None: + orientations_wp = wp.from_torch(orientations) + else: + orientations_wp = wp.zeros((0, 4), dtype=wp.float32).to(self._device) + + # Dummy array for scales (not modifying) + scales_wp = wp.zeros((0, 3), dtype=wp.float32).to(self._device) + + # Use cached fabricarray for world matrices + world_matrices = self._fabric_world_matrices + + # Batch compose matrices with a single kernel launch + wp.launch( + kernel=fabric_utils.compose_fabric_transformation_matrix_from_warp_arrays, + dim=count, + inputs=[ + world_matrices, + positions_wp, + orientations_wp, + scales_wp, # dummy array instead of None + False, # broadcast_positions + False, # broadcast_orientations + False, # broadcast_scales + indices_wp, + self._view_to_fabric, + ], + device=self._device, + ) + + # Synchronize to ensure kernel completes + wp.synchronize() + + # Update world transforms within Fabric hierarchy + self._fabric_hierarchy.update_world_xforms() + # Fabric now has authoritative data; skip future USD syncs + self._fabric_usd_sync_done = True + # Mirror to USD for renderer-facing prims when enabled. + if self._sync_usd_on_fabric_write: + self._set_world_poses_usd(positions, orientations, indices) + + # Fabric writes are GPU-resident; local pose operations still use USD. + + def _set_local_poses_fabric( + self, + translations: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + indices: Sequence[int] | None = None, + ): + """Set local poses using USD (matches Isaac Sim's design). + + Note: Even in Fabric mode, local pose operations use USD. + This is Isaac Sim's design: the ``usd=False`` parameter only affects world poses. + + Rationale: + - Local pose writes need correct parent-child hierarchy relationships + - USD maintains these relationships correctly and efficiently + - Fabric is optimized for world pose operations, not local hierarchies + """ + self._set_local_poses_usd(translations, orientations, indices) + + def _set_scales_fabric(self, scales: torch.Tensor, indices: Sequence[int] | None = None): + """Set scales using Fabric GPU batch operations.""" + # Lazy initialization + if not self._fabric_initialized: + self._initialize_fabric() + + # Resolve indices (treat slice(None) as None for consistency with USD path) + indices_wp = self._resolve_indices_wp(indices) + + count = indices_wp.shape[0] + + # Convert torch to warp + scales_wp = wp.from_torch(scales) + + # Dummy arrays for positions and orientations (not modifying) + positions_wp = wp.zeros((0, 3), dtype=wp.float32).to(self._device) + orientations_wp = wp.zeros((0, 4), dtype=wp.float32).to(self._device) + + # Use cached fabricarray for world matrices + world_matrices = self._fabric_world_matrices + + # Batch compose matrices on GPU with a single kernel launch + wp.launch( + kernel=fabric_utils.compose_fabric_transformation_matrix_from_warp_arrays, + dim=count, + inputs=[ + world_matrices, + positions_wp, # dummy array instead of None + orientations_wp, # dummy array instead of None + scales_wp, + False, # broadcast_positions + False, # broadcast_orientations + False, # broadcast_scales + indices_wp, + self._view_to_fabric, + ], + device=self._device, + ) + + # Synchronize to ensure kernel completes before syncing + wp.synchronize() + + # Update world transforms to propagate changes + self._fabric_hierarchy.update_world_xforms() + # Fabric now has authoritative data; skip future USD syncs + self._fabric_usd_sync_done = True + # Mirror to USD for renderer-facing prims when enabled. + if self._sync_usd_on_fabric_write: + self._set_scales_usd(scales, indices) + + def _get_world_poses_fabric(self, indices: Sequence[int] | None = None) -> tuple[torch.Tensor, torch.Tensor]: + """Get world poses from Fabric using GPU batch operations.""" + # Lazy initialization of Fabric infrastructure + if not self._fabric_initialized: + self._initialize_fabric() + # Sync once from USD to ensure reads see the latest authored transforms + if not self._fabric_usd_sync_done: + self._sync_fabric_from_usd_once() + + # Resolve indices (treat slice(None) as None for consistency with USD path) + indices_wp = self._resolve_indices_wp(indices) + + count = indices_wp.shape[0] + + # Use pre-allocated buffers for full reads, allocate only for partial reads + use_cached_buffers = indices is None or indices == slice(None) + if use_cached_buffers: + # Full read: Use cached buffers (zero allocation overhead!) + positions_wp = self._fabric_positions_buffer + orientations_wp = self._fabric_orientations_buffer + scales_wp = self._fabric_dummy_buffer + else: + # Partial read: Need to allocate buffers of appropriate size + positions_wp = wp.zeros((count, 3), dtype=wp.float32).to(self._device) + orientations_wp = wp.zeros((count, 4), dtype=wp.float32).to(self._device) + scales_wp = self._fabric_dummy_buffer # Always use dummy for scales + + # Use cached fabricarray for world matrices + # This eliminates the 0.06-0.30ms variability from creating fabricarray each call + world_matrices = self._fabric_world_matrices + + # Launch GPU kernel to decompose matrices in parallel + wp.launch( + kernel=fabric_utils.decompose_fabric_transformation_matrix_to_warp_arrays, + dim=count, + inputs=[ + world_matrices, + positions_wp, + orientations_wp, + scales_wp, # dummy array instead of None + indices_wp, + self._view_to_fabric, + ], + device=self._device, + ) + + # Return tensors: zero-copy for cached buffers, conversion for partial reads + if use_cached_buffers: + # Zero-copy! The Warp kernel wrote directly into the PyTorch tensors + # We just need to synchronize to ensure the kernel is done + wp.synchronize() + return self._fabric_positions_torch, self._fabric_orientations_torch + else: + # Partial read: Need to convert from Warp to torch + positions = wp.to_torch(positions_wp) + orientations = wp.to_torch(orientations_wp) + return positions, orientations + + def _get_local_poses_fabric(self, indices: Sequence[int] | None = None) -> tuple[torch.Tensor, torch.Tensor]: + """Get local poses using USD (matches Isaac Sim's design). + + Note: + Even in Fabric mode, local pose operations use USD's XformCache. + This is Isaac Sim's design: the ``usd=False`` parameter only affects world poses. + + Rationale: + - Local pose computation requires parent transforms which may not be in the view + - USD's XformCache provides efficient hierarchy-aware local transform queries + - Fabric is optimized for world pose operations, not local hierarchies + """ + return self._get_local_poses_usd(indices) + + def _get_scales_fabric(self, indices: Sequence[int] | None = None) -> torch.Tensor: + """Get scales from Fabric using GPU batch operations.""" + # Lazy initialization + if not self._fabric_initialized: + self._initialize_fabric() + # Sync once from USD to ensure reads see the latest authored transforms + if not self._fabric_usd_sync_done: + self._sync_fabric_from_usd_once() + + # Resolve indices (treat slice(None) as None for consistency with USD path) + indices_wp = self._resolve_indices_wp(indices) + + count = indices_wp.shape[0] + + # Use pre-allocated buffers for full reads, allocate only for partial reads + use_cached_buffers = indices is None or indices == slice(None) + if use_cached_buffers: + # Full read: Use cached buffers (zero allocation overhead!) + scales_wp = self._fabric_scales_buffer + else: + # Partial read: Need to allocate buffer of appropriate size + scales_wp = wp.zeros((count, 3), dtype=wp.float32).to(self._device) + + # Always use dummy buffers for positions and orientations (not needed for scales) + positions_wp = self._fabric_dummy_buffer + orientations_wp = self._fabric_dummy_buffer + + # Use cached fabricarray for world matrices + world_matrices = self._fabric_world_matrices + + # Launch GPU kernel to decompose matrices in parallel + wp.launch( + kernel=fabric_utils.decompose_fabric_transformation_matrix_to_warp_arrays, + dim=count, + inputs=[ + world_matrices, + positions_wp, # dummy array instead of None + orientations_wp, # dummy array instead of None + scales_wp, + indices_wp, + self._view_to_fabric, + ], + device=self._device, + ) + + # Return tensor: zero-copy for cached buffers, conversion for partial reads + if use_cached_buffers: + # Zero-copy! The Warp kernel wrote directly into the PyTorch tensor + wp.synchronize() + return self._fabric_scales_torch + else: + # Partial read: Need to convert from Warp to torch + return wp.to_torch(scales_wp) + + """ + Internal Functions - Initialization. + """ + + def _initialize_fabric(self) -> None: + """Initialize Fabric batch infrastructure for GPU-accelerated pose queries. + + This method ensures all prims have the required Fabric hierarchy attributes + (``omni:fabric:localMatrix`` and ``omni:fabric:worldMatrix``) and creates the necessary + infrastructure for batch GPU operations using Warp. + + Based on the Fabric Hierarchy documentation, when Fabric Scene Delegate is enabled, + all boundable prims should have these attributes. This method ensures they exist + and are properly synchronized with USD. + """ + import usdrt + from usdrt import Rt + + # Get USDRT (Fabric) stage + stage_id = sim_utils.get_current_stage_id() + fabric_stage = usdrt.Usd.Stage.Attach(stage_id) + + # Step 1: Ensure all prims have Fabric hierarchy attributes + # According to the documentation, these attributes are created automatically + # when Fabric Scene Delegate is enabled, but we ensure they exist + for i in range(self.count): + rt_prim = fabric_stage.GetPrimAtPath(self.prim_paths[i]) + rt_xformable = Rt.Xformable(rt_prim) + + # Create Fabric hierarchy world matrix attribute if it doesn't exist + has_attr = ( + rt_xformable.HasFabricHierarchyWorldMatrixAttr() + if hasattr(rt_xformable, "HasFabricHierarchyWorldMatrixAttr") + else False + ) + if not has_attr: + rt_xformable.CreateFabricHierarchyWorldMatrixAttr() + + # Best-effort USD->Fabric sync; authoritative initialization happens on first read. + rt_xformable.SetWorldXformFromUsd() + + # Create view index attribute for batch operations + rt_prim.CreateAttribute(self._view_index_attr, usdrt.Sdf.ValueTypeNames.UInt, custom=True) + rt_prim.GetAttribute(self._view_index_attr).Set(i) + + # After syncing all prims, update the Fabric hierarchy to ensure world matrices are computed + self._fabric_hierarchy = usdrt.hierarchy.IFabricHierarchy().get_fabric_hierarchy( + fabric_stage.GetFabricId(), fabric_stage.GetStageIdAsStageId() + ) + self._fabric_hierarchy.update_world_xforms() + + # Step 2: Create index arrays for batch operations + self._default_view_indices = wp.zeros((self.count,), dtype=wp.uint32).to(self._device) + wp.launch( + kernel=fabric_utils.arange_k, + dim=self.count, + inputs=[self._default_view_indices], + device=self._device, + ) + wp.synchronize() # Ensure indices are ready + + # Step 3: Create Fabric selection with attribute filtering + # SelectPrims expects device format like "cuda:0" not "cuda" + # + # KNOWN ISSUE: SelectPrims may return prims in a different order than self._prims + # (which comes from USD's find_matching_prims). We create a bidirectional mapping + # (_view_to_fabric and _fabric_to_view) to handle this ordering difference. + # This works correctly for full-view operations but partial indexing still has issues. + fabric_device = self._device + if self._device == "cuda": + logger.warning("Fabric device is not specified, defaulting to 'cuda:0'.") + fabric_device = "cuda:0" + + self._fabric_selection = fabric_stage.SelectPrims( + require_attrs=[ + (usdrt.Sdf.ValueTypeNames.UInt, self._view_index_attr, usdrt.Usd.Access.Read), + (usdrt.Sdf.ValueTypeNames.Matrix4d, "omni:fabric:worldMatrix", usdrt.Usd.Access.ReadWrite), + ], + device=fabric_device, + ) + + # Step 4: Create bidirectional mapping between view and fabric indices + self._view_to_fabric = wp.zeros((self.count,), dtype=wp.uint32).to(self._device) + self._fabric_to_view = wp.fabricarray(self._fabric_selection, self._view_index_attr) + + wp.launch( + kernel=fabric_utils.set_view_to_fabric_array, + dim=self._fabric_to_view.shape[0], + inputs=[self._fabric_to_view, self._view_to_fabric], + device=self._device, + ) + # Synchronize to ensure mapping is ready before any operations + wp.synchronize() + + # Pre-allocate reusable output buffers for read operations + self._fabric_positions_torch = torch.zeros((self.count, 3), dtype=torch.float32, device=self._device) + self._fabric_orientations_torch = torch.zeros((self.count, 4), dtype=torch.float32, device=self._device) + self._fabric_scales_torch = torch.zeros((self.count, 3), dtype=torch.float32, device=self._device) + + # Create Warp views of the PyTorch tensors + self._fabric_positions_buffer = wp.from_torch(self._fabric_positions_torch, dtype=wp.float32) + self._fabric_orientations_buffer = wp.from_torch(self._fabric_orientations_torch, dtype=wp.float32) + self._fabric_scales_buffer = wp.from_torch(self._fabric_scales_torch, dtype=wp.float32) + + # Dummy array for unused outputs (always empty) + self._fabric_dummy_buffer = wp.zeros((0, 3), dtype=wp.float32).to(self._device) + + # Cache fabricarray for world matrices to avoid recreation overhead + # Refs: https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/usdrt_prim_selection.html + # https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/scenegraph_use.html + self._fabric_world_matrices = wp.fabricarray(self._fabric_selection, "omni:fabric:worldMatrix") + + # Cache Fabric stage to avoid expensive get_current_stage() calls + self._fabric_stage = fabric_stage + + self._fabric_initialized = True + # Force a one-time USD->Fabric sync on first read to pick up any USD edits + # made after the view was constructed. + self._fabric_usd_sync_done = False + + def _sync_fabric_from_usd_once(self) -> None: + """Sync Fabric world matrices from USD once, on the first read.""" + # Ensure Fabric is initialized + if not self._fabric_initialized: + self._initialize_fabric() + + # Ensure authored USD transforms are flushed before reading into Fabric. + sim_utils.update_stage() + + # Read authoritative transforms from USD and write once into Fabric. + positions_usd, orientations_usd = self._get_world_poses_usd() + scales_usd = self._get_scales_usd() + + prev_sync = self._sync_usd_on_fabric_write + self._sync_usd_on_fabric_write = False + self._set_world_poses_fabric(positions_usd, orientations_usd) + self._set_scales_fabric(scales_usd) + self._sync_usd_on_fabric_write = prev_sync + + self._fabric_usd_sync_done = True + + def _resolve_indices_wp(self, indices: Sequence[int] | None) -> wp.array: + """Resolve view indices as a Warp array.""" + if indices is None or indices == slice(None): + if self._default_view_indices is None: + raise RuntimeError("Fabric indices are not initialized.") + return self._default_view_indices + indices_list = indices.tolist() if isinstance(indices, torch.Tensor) else list(indices) + return wp.array(indices_list, dtype=wp.uint32).to(self._device) diff --git a/source/isaaclab/isaaclab/terrains/__init__.py b/source/isaaclab/isaaclab/terrains/__init__.py index b74565fdad42..6f0b50185572 100644 --- a/source/isaaclab/isaaclab/terrains/__init__.py +++ b/source/isaaclab/isaaclab/terrains/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/terrains/config/__init__.py b/source/isaaclab/isaaclab/terrains/config/__init__.py index e14a6c6901e8..264189e183b4 100644 --- a/source/isaaclab/isaaclab/terrains/config/__init__.py +++ b/source/isaaclab/isaaclab/terrains/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/terrains/config/rough.py b/source/isaaclab/isaaclab/terrains/config/rough.py index 3b2a16f7aba9..fde3bf8408b4 100644 --- a/source/isaaclab/isaaclab/terrains/config/rough.py +++ b/source/isaaclab/isaaclab/terrains/config/rough.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/terrains/height_field/__init__.py b/source/isaaclab/isaaclab/terrains/height_field/__init__.py index 589798cf25b6..3bc28ba3ccfe 100644 --- a/source/isaaclab/isaaclab/terrains/height_field/__init__.py +++ b/source/isaaclab/isaaclab/terrains/height_field/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py b/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py index d9ff255c3b51..3869eae25c3f 100644 --- a/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py +++ b/source/isaaclab/isaaclab/terrains/height_field/hf_terrains.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +7,10 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np import scipy.interpolate as interpolate -from typing import TYPE_CHECKING from .utils import height_field_to_mesh diff --git a/source/isaaclab/isaaclab/terrains/height_field/hf_terrains_cfg.py b/source/isaaclab/isaaclab/terrains/height_field/hf_terrains_cfg.py index 8fd49077aa26..df1d6dcc21a2 100644 --- a/source/isaaclab/isaaclab/terrains/height_field/hf_terrains_cfg.py +++ b/source/isaaclab/isaaclab/terrains/height_field/hf_terrains_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/terrains/height_field/utils.py b/source/isaaclab/isaaclab/terrains/height_field/utils.py index 42ae15693e97..256e8129fe34 100644 --- a/source/isaaclab/isaaclab/terrains/height_field/utils.py +++ b/source/isaaclab/isaaclab/terrains/height_field/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,12 @@ import copy import functools -import numpy as np -import trimesh from collections.abc import Callable from typing import TYPE_CHECKING +import numpy as np +import trimesh + if TYPE_CHECKING: from .hf_terrains_cfg import HfTerrainBaseCfg diff --git a/source/isaaclab/isaaclab/terrains/sub_terrain_cfg.py b/source/isaaclab/isaaclab/terrains/sub_terrain_cfg.py index 1e2fa8330459..0dab3ea8f3c1 100644 --- a/source/isaaclab/isaaclab/terrains/sub_terrain_cfg.py +++ b/source/isaaclab/isaaclab/terrains/sub_terrain_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,11 +6,12 @@ from __future__ import annotations -import numpy as np -import trimesh from collections.abc import Callable from dataclasses import MISSING +import numpy as np +import trimesh + from isaaclab.utils import configclass diff --git a/source/isaaclab/isaaclab/terrains/terrain_generator.py b/source/isaaclab/isaaclab/terrains/terrain_generator.py index 456aa88c1740..58c0c85be9d1 100644 --- a/source/isaaclab/isaaclab/terrains/terrain_generator.py +++ b/source/isaaclab/isaaclab/terrains/terrain_generator.py @@ -1,17 +1,17 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import numpy as np +import logging import os -import torch -import trimesh from typing import TYPE_CHECKING -import omni.log +import numpy as np +import torch +import trimesh from isaaclab.utils.dict import dict_to_md5_hash from isaaclab.utils.io import dump_yaml @@ -25,6 +25,9 @@ from .sub_terrain_cfg import FlatPatchSamplingCfg, SubTerrainBaseCfg from .terrain_generator_cfg import TerrainGeneratorCfg +# import logger +logger = logging.getLogger(__name__) + class TerrainGenerator: r"""Terrain generator to handle different terrain generation functions. @@ -50,21 +53,23 @@ class TerrainGenerator: .. math:: - \text{difficulty} = \frac{\text{row_id} + \eta}{\text{num_rows}} \times (\text{upper} - \text{lower}) + \text{lower} + \text{difficulty} = + \frac{\text{row_id} + \eta}{\text{num_rows}} \times (\text{upper} - \text{lower}) + \text{lower} where :math:`\eta\sim\mathcal{U}(0, 1)` is a random perturbation to the difficulty, and :math:`(\text{lower}, \text{upper})` is the range of the difficulty parameter, specified using the :attr:`~TerrainGeneratorCfg.difficulty_range` parameter. If a curriculum is not used, the terrains are generated randomly. In this case, the difficulty parameter - is randomly sampled from the specified range, given by the :attr:`~TerrainGeneratorCfg.difficulty_range` parameter: + is randomly sampled from the specified range, given by the :attr:`~TerrainGeneratorCfg.difficulty_range` + parameter: .. math:: \text{difficulty} \sim \mathcal{U}(\text{lower}, \text{upper}) - If the :attr:`~TerrainGeneratorCfg.flat_patch_sampling` is specified for a sub-terrain, flat patches are sampled - on the terrain. These can be used for spawning robots, targets, etc. The sampled patches are stored + If the :attr:`~TerrainGeneratorCfg.flat_patch_sampling` is specified for a sub-terrain, flat patches are + sampled on the terrain. These can be used for spawning robots, targets, etc. The sampled patches are stored in the :obj:`flat_patches` dictionary. The key specifies the intention of the flat patches and the value is a tensor containing the flat patches for each sub-terrain. @@ -126,7 +131,7 @@ def __init__(self, cfg: TerrainGeneratorCfg, device: str = "cpu"): # throw a warning if the cache is enabled but the seed is not set if self.cfg.use_cache and self.cfg.seed is None: - omni.log.warn( + logger.warning( "Cache is enabled but the seed is not set. The terrain generation will not be reproducible." " Please set the seed in the terrain generator configuration to make the generation reproducible." ) @@ -302,7 +307,7 @@ def _add_sub_terrain( """ # sample flat patches if specified if sub_terrain_cfg.flat_patch_sampling is not None: - omni.log.info(f"Sampling flat patches for sub-terrain at (row, col): ({row}, {col})") + logger.info(f"Sampling flat patches for sub-terrain at (row, col): ({row}, {col})") # convert the mesh to warp mesh wp_mesh = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=self.device) # sample flat patches based on each patch configuration for that sub-terrain diff --git a/source/isaaclab/isaaclab/terrains/terrain_generator_cfg.py b/source/isaaclab/isaaclab/terrains/terrain_generator_cfg.py index 71cf4ddaa78c..6a3238c7cb4a 100644 --- a/source/isaaclab/isaaclab/terrains/terrain_generator_cfg.py +++ b/source/isaaclab/isaaclab/terrains/terrain_generator_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -64,7 +64,9 @@ class TerrainGeneratorCfg: """The height of the border around the terrain (in m). Defaults to 1.0. .. note:: - The default border extends below the ground. If you want to make the border above the ground, choose a negative value. + The default border extends below the ground. If you want to make the border above the ground, + choose a negative value. + """ num_rows: int = 1 diff --git a/source/isaaclab/isaaclab/terrains/terrain_importer.py b/source/isaaclab/isaaclab/terrains/terrain_importer.py index a8a2712f2514..e9ddc691b35b 100644 --- a/source/isaaclab/isaaclab/terrains/terrain_importer.py +++ b/source/isaaclab/isaaclab/terrains/terrain_importer.py @@ -1,16 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +import logging +from typing import TYPE_CHECKING + import numpy as np import torch import trimesh -from typing import TYPE_CHECKING - -import omni.log import isaaclab.sim as sim_utils from isaaclab.markers import VisualizationMarkers @@ -21,6 +21,9 @@ if TYPE_CHECKING: from .terrain_importer_cfg import TerrainImporterCfg +# import logger +logger = logging.getLogger(__name__) + class TerrainImporter: r"""A class to handle terrain meshes and import them into the simulator. @@ -87,8 +90,11 @@ def __init__(self, cfg: TerrainImporterCfg): cfg=self.cfg.terrain_generator, device=self.device ) self.import_mesh("terrain", terrain_generator.terrain_mesh) - # configure the terrain origins based on the terrain generator - self.configure_env_origins(terrain_generator.terrain_origins) + if self.cfg.use_terrain_origins: + # configure the terrain origins based on the terrain generator + self.configure_env_origins(terrain_generator.terrain_origins) + else: + self.configure_env_origins() # refer to the flat patches self._terrain_flat_patches = terrain_generator.flat_patches elif self.cfg.terrain_type == "usd": @@ -208,7 +214,7 @@ def import_ground_plane(self, name: str, size: tuple[float, float] = (2.0e6, 2.0 if "diffuse_color" in material: color = material["diffuse_color"] else: - omni.log.warn( + logger.warning( "Visual material specified for ground plane but no diffuse color found." " Using default color: (0.0, 0.0, 0.0)" ) @@ -373,7 +379,7 @@ def warp_meshes(self): .. deprecated:: v2.1.0 The `warp_meshes` attribute is deprecated. It is no longer stored inside the class. """ - omni.log.warn( + logger.warning( "The `warp_meshes` attribute is deprecated. It is no longer stored inside the `TerrainImporter` class." " Returning an empty dictionary." ) @@ -386,7 +392,7 @@ def meshes(self) -> dict[str, trimesh.Trimesh]: .. deprecated:: v2.1.0 The `meshes` attribute is deprecated. It is no longer stored inside the class. """ - omni.log.warn( + logger.warning( "The `meshes` attribute is deprecated. It is no longer stored inside the `TerrainImporter` class." " Returning an empty dictionary." ) diff --git a/source/isaaclab/isaaclab/terrains/terrain_importer_cfg.py b/source/isaaclab/isaaclab/terrains/terrain_importer_cfg.py index 7afc63aefa16..7b42e06caaf3 100644 --- a/source/isaaclab/isaaclab/terrains/terrain_importer_cfg.py +++ b/source/isaaclab/isaaclab/terrains/terrain_importer_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -65,7 +65,16 @@ class TerrainImporterCfg: """The spacing between environment origins when defined in a grid. Defaults to None. Note: - This parameter is used only when the ``terrain_type`` is "plane" or "usd". + This parameter is used only when the ``terrain_type`` is "plane" or "usd" or if + :attr:`use_terrain_origins` is False. + """ + + use_terrain_origins: bool = True + """Whether to set the environment origins based on the terrain origins or in a grid + according to :attr:`env_spacing`. Defaults to True. + + Note: + This parameter is used only when the :attr:`terrain type` is "generator". """ visual_material: sim_utils.VisualMaterialCfg | None = sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 0.0)) diff --git a/source/isaaclab/isaaclab/terrains/trimesh/__init__.py b/source/isaaclab/isaaclab/terrains/trimesh/__init__.py index 98f0b0f3ed57..b27b7a921109 100644 --- a/source/isaaclab/isaaclab/terrains/trimesh/__init__.py +++ b/source/isaaclab/isaaclab/terrains/trimesh/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py b/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py index e4ab2ce3ba74..d5a327aebe58 100644 --- a/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py +++ b/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np import scipy.spatial.transform as tf import torch import trimesh -from typing import TYPE_CHECKING from .utils import * # noqa: F401, F403 from .utils import make_border, make_plane @@ -253,9 +254,9 @@ def random_grid_terrain( """Generate a terrain with cells of random heights and fixed width. The terrain is generated in the x-y plane and has a height of 1.0. It is then divided into a grid of the - specified size :obj:`cfg.grid_width`. Each grid cell is then randomly shifted in the z-direction by a value uniformly - sampled between :obj:`cfg.grid_height_range`. At the center of the terrain, a platform of the specified width - :obj:`cfg.platform_width` is generated. + specified size :obj:`cfg.grid_width`. Each grid cell is then randomly shifted in the z-direction by a value + uniformly sampled between :obj:`cfg.grid_height_range`. At the center of the terrain, a platform of the specified + width :obj:`cfg.platform_width` is generated. If :obj:`cfg.holes` is True, the terrain will have randomized grid cells only along the plane extending from the platform (like a plus sign). The remaining area remains empty and no border will be added. @@ -397,7 +398,7 @@ def rails_terrain( A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). """ # resolve the terrain configuration - rail_height = cfg.rail_height_range[1] - difficulty * (cfg.rail_height_range[1] - cfg.rail_height_range[0]) + rail_height = cfg.rail_height_range[0] + difficulty * (cfg.rail_height_range[1] - cfg.rail_height_range[0]) # initialize list of meshes meshes_list = list() @@ -810,10 +811,12 @@ def repeated_objects_terrain( meshes_list = list() # compute quantities origin = np.asarray((0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.5 * platform_height)) - platform_corners = np.asarray([ - [origin[0] - cfg.platform_width / 2, origin[1] - cfg.platform_width / 2], - [origin[0] + cfg.platform_width / 2, origin[1] + cfg.platform_width / 2], - ]) + platform_corners = np.asarray( + [ + [origin[0] - cfg.platform_width / 2, origin[1] - cfg.platform_width / 2], + [origin[0] + cfg.platform_width / 2, origin[1] + cfg.platform_width / 2], + ] + ) platform_corners[0, :] *= 1 - platform_clearance platform_corners[1, :] *= 1 + platform_clearance # sample valid center for objects diff --git a/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains_cfg.py b/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains_cfg.py index 103aa18424dc..4247e21486be 100644 --- a/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains_cfg.py +++ b/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -221,10 +221,14 @@ class ObjectCfg: """"This parameter is deprecated, but stated here to support backward compatibility""" abs_height_noise: tuple[float, float] = (0.0, 0.0) - """The minimum and maximum amount of additive noise for the height of the objects. Default is set to 0.0, which is no noise.""" + """The minimum and maximum amount of additive noise for the height of the objects. Default is set to 0.0, + which is no noise. + """ rel_height_noise: tuple[float, float] = (1.0, 1.0) - """The minimum and maximum amount of multiplicative noise for the height of the objects. Default is set to 1.0, which is no noise.""" + """The minimum and maximum amount of multiplicative noise for the height of the objects. Default is set to 1.0, + which is no noise. + """ platform_width: float = 1.0 """The width of the cylindrical platform at the center of the terrain. Defaults to 1.0.""" diff --git a/source/isaaclab/isaaclab/terrains/trimesh/utils.py b/source/isaaclab/isaaclab/terrains/trimesh/utils.py index b9577ee2c50a..aede42f3b7da 100644 --- a/source/isaaclab/isaaclab/terrains/trimesh/utils.py +++ b/source/isaaclab/isaaclab/terrains/trimesh/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/terrains/utils.py b/source/isaaclab/isaaclab/terrains/utils.py index 1c55a9325b26..0feee6ca51f3 100644 --- a/source/isaaclab/isaaclab/terrains/utils.py +++ b/source/isaaclab/isaaclab/terrains/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 @@ import numpy as np import torch import trimesh - import warp as wp from isaaclab.utils.warp import raycast_mesh @@ -80,15 +79,14 @@ def create_prim_from_mesh(prim_path: str, mesh: trimesh.Trimesh, **kwargs): physics_material: The physics material to apply. Defaults to None. """ # need to import these here to prevent isaacsim launching when importing this module - import isaacsim.core.utils.prims as prim_utils from pxr import UsdGeom import isaaclab.sim as sim_utils # create parent prim - prim_utils.create_prim(prim_path, "Xform") + sim_utils.create_prim(prim_path, "Xform") # create mesh prim - prim = prim_utils.create_prim( + prim = sim_utils.create_prim( f"{prim_path}/mesh", "Mesh", translation=kwargs.get("translation"), diff --git a/source/isaaclab/isaaclab/ui/widgets/__init__.py b/source/isaaclab/isaaclab/ui/widgets/__init__.py index 74381c170231..cfd6de31fa2b 100644 --- a/source/isaaclab/isaaclab/ui/widgets/__init__.py +++ b/source/isaaclab/isaaclab/ui/widgets/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/ui/widgets/image_plot.py b/source/isaaclab/isaaclab/ui/widgets/image_plot.py index 5b61daae85f8..939ef01dfa9f 100644 --- a/source/isaaclab/isaaclab/ui/widgets/image_plot.py +++ b/source/isaaclab/isaaclab/ui/widgets/image_plot.py @@ -1,16 +1,18 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np +from __future__ import annotations + +import logging from contextlib import suppress +from typing import TYPE_CHECKING + +import numpy as np from matplotlib import cm -from typing import TYPE_CHECKING, Optional -import carb import omni -import omni.log with suppress(ImportError): # isaacsim.gui is not available when running in headless mode. @@ -22,6 +24,9 @@ import isaacsim.gui.components import omni.ui +# import logger +logger = logging.getLogger(__name__) + class ImagePlot(UIWidgetWrapper): """An image plot widget to display live data. @@ -41,7 +46,9 @@ class ImagePlot(UIWidgetWrapper): |||+-------------------------------------------------+||| ||| mode_frame ||| ||| ||| - ||| [x][Absolute] [x][Grayscaled] [ ][Colorized] ||| + ||| [Dropdown: Mode Selection] ||| + ||| [Collapsible: Manual Normalization Options] ||| + ||+---------------------------------------------------+|| |+-----------------------------------------------------+| +-------------------------------------------------------+ @@ -49,11 +56,11 @@ class ImagePlot(UIWidgetWrapper): def __init__( self, - image: Optional[np.ndarray] = None, + image: np.ndarray | None = None, label: str = "", widget_height: int = 200, - show_min_max: bool = True, - unit: tuple[float, str] = (1, ""), + min_value: float = 0.0, + max_value: float = 1.0, ): """Create an XY plot UI Widget with axis scaling, legends, and support for multiple plots. @@ -64,12 +71,9 @@ def __init__( image: Image to display label: Short descriptive text to the left of the plot widget_height: Height of the plot in pixels - show_min_max: Whether to show the min and max values of the image - unit: Tuple of (scale, name) for the unit of the image + min_value: Minimum value for manual normalization/colorization. Defaults to 0.0. + max_value: Maximum value for manual normalization/colorization. Defaults to 1.0. """ - self._show_min_max = show_min_max - self._unit_scale = unit[0] - self._unit_name = unit[1] self._curr_mode = "None" @@ -79,7 +83,7 @@ def __init__( self._byte_provider = omni.ui.ByteImageProvider() if image is None: - carb.log_warn("image is NONE") + logger.warning("image is NONE") image = np.ones((480, 640, 3), dtype=np.uint8) * 255 image[:, :, 0] = 0 image[:, :240, 1] = 0 @@ -115,7 +119,7 @@ def update_image(self, image: np.ndarray): image = (image * 255).astype(np.uint8) elif self._curr_mode == "Colorization": if image.ndim == 3 and image.shape[2] == 3: - omni.log.warn("Colorization mode is only available for single channel images") + logger.warning("Colorization mode is only available for single channel images") else: image = (image - image.min()) / (image.max() - image.min()) colormap = cm.get_cmap("jet") @@ -155,7 +159,6 @@ def _get_unit_description(self, min_value: float, max_value: float, median_value ) def _build_widget(self): - with omni.ui.VStack(spacing=3): with omni.ui.HStack(): # Write the leftmost label for what this plot is @@ -184,25 +187,124 @@ def _build_mode_frame(self): The built widget has the following layout: +-------------------------------------------------------+ - | legends_frame | + | mode_frame | ||+---------------------------------------------------+|| - ||| ||| - ||| [x][Series 1] [x][Series 2] [ ][Series 3] ||| - ||| ||| + ||| [Dropdown: Mode Selection] ||| + ||| [Collapsible: Manual Normalization Options] ||| |||+-------------------------------------------------+||| |+-----------------------------------------------------+| +-------------------------------------------------------+ """ - with omni.ui.HStack(): - with omni.ui.HStack(): - - def _change_mode(value): - self._curr_mode = value - - isaacsim.gui.components.ui_utils.dropdown_builder( - label="Mode", - type="dropdown", - items=["Original", "Normalization", "Colorization"], - tooltip="Select a mode", - on_clicked_fn=_change_mode, - ) + with omni.ui.VStack(spacing=5, style=isaacsim.gui.components.ui_utils.get_style()): + + def _change_mode(value): + self._curr_mode = value + # Update visibility of collapsible frame + show_options = value in ["Normalization", "Colorization"] + if hasattr(self, "_options_collapsable"): + self._options_collapsable.visible = show_options + if show_options: + self._options_collapsable.title = f"{value} Options" + + # Mode dropdown + isaacsim.gui.components.ui_utils.dropdown_builder( + label="Mode", + type="dropdown", + items=["Original", "Normalization", "Colorization"], + tooltip="Select a mode", + on_clicked_fn=_change_mode, + ) + + # Collapsible frame for options (initially hidden) + self._options_collapsable = omni.ui.CollapsableFrame( + "Normalization Options", + height=0, + collapsed=False, + visible=False, + style=isaacsim.gui.components.ui_utils.get_style(), + style_type_name_override="CollapsableFrame", + ) + + with self._options_collapsable: + with omni.ui.VStack(spacing=5, style=isaacsim.gui.components.ui_utils.get_style()): + + def _on_manual_changed(enabled): + self._enabled_min_max = enabled + # Enable/disable the float fields + if hasattr(self, "_min_model"): + self._min_field.enabled = enabled + if hasattr(self, "_max_model"): + self._max_field.enabled = enabled + + def _on_min_changed(model): + self._min_value = model.as_float + + def _on_max_changed(model): + self._max_value = model.as_float + + # Manual values checkbox + isaacsim.gui.components.ui_utils.cb_builder( + label="Use Manual Values", + type="checkbox", + default_val=self._enabled_min_max, + tooltip="Enable manual min/max values", + on_clicked_fn=_on_manual_changed, + ) + + # Min value with reset button + with omni.ui.HStack(): + omni.ui.Label( + "Min", + width=isaacsim.gui.components.ui_utils.LABEL_WIDTH, + alignment=omni.ui.Alignment.LEFT_CENTER, + ) + self._min_field = omni.ui.FloatDrag( + name="FloatField", + width=omni.ui.Fraction(1), + height=0, + alignment=omni.ui.Alignment.LEFT_CENTER, + enabled=self._enabled_min_max, + ) + self._min_model = self._min_field.model + self._min_model.set_value(self._min_value) + self._min_model.add_value_changed_fn(_on_min_changed) + + omni.ui.Spacer(width=5) + omni.ui.Button( + "0", + width=20, + height=20, + clicked_fn=lambda: self._min_model.set_value(0.0), + tooltip="Reset to 0.0", + style=isaacsim.gui.components.ui_utils.get_style(), + ) + isaacsim.gui.components.ui_utils.add_line_rect_flourish(False) + + # Max value with reset button + with omni.ui.HStack(): + omni.ui.Label( + "Max", + width=isaacsim.gui.components.ui_utils.LABEL_WIDTH, + alignment=omni.ui.Alignment.LEFT_CENTER, + ) + self._max_field = omni.ui.FloatDrag( + name="FloatField", + width=omni.ui.Fraction(1), + height=0, + alignment=omni.ui.Alignment.LEFT_CENTER, + enabled=self._enabled_min_max, + ) + self._max_model = self._max_field.model + self._max_model.set_value(self._max_value) + self._max_model.add_value_changed_fn(_on_max_changed) + + omni.ui.Spacer(width=5) + omni.ui.Button( + "1", + width=20, + height=20, + clicked_fn=lambda: self._max_model.set_value(1.0), + tooltip="Reset to 1.0", + style=isaacsim.gui.components.ui_utils.get_style(), + ) + isaacsim.gui.components.ui_utils.add_line_rect_flourish(False) diff --git a/source/isaaclab/isaaclab/ui/widgets/line_plot.py b/source/isaaclab/isaaclab/ui/widgets/line_plot.py index e31410e6b6bd..1f5e315a07cf 100644 --- a/source/isaaclab/isaaclab/ui/widgets/line_plot.py +++ b/source/isaaclab/isaaclab/ui/widgets/line_plot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,12 +6,14 @@ from __future__ import annotations import colorsys -import numpy as np from contextlib import suppress from typing import TYPE_CHECKING +import numpy as np + 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. @@ -73,13 +75,16 @@ def __init__( """Create a new LiveLinePlot widget. Args: - y_data: A list of lists of floats containing the data to plot. Each list of floats represents a series in the plot. + y_data: A list of lists of floats containing the data to plot. Each list of floats represents a + series in the plot. y_min: The minimum y value to display. Defaults to -10. y_max: The maximum y value to display. Defaults to 10. plot_height: The height of the plot in pixels. Defaults to 150. show_legend: Whether to display the legend. Defaults to True. - legends: A list of strings containing the legend labels for each series. If None, the default labels are "Series_0", "Series_1", etc. Defaults to None. - max_datapoints: The maximum number of data points to display. If the number of data points exceeds this value, the oldest data points are removed. Defaults to 200. + legends: A list of strings containing the legend labels for each series. If None, the default + labels are "Series_0", "Series_1", etc. Defaults to None. + max_datapoints: The maximum number of data points to display. If the number of data points exceeds + this value, the oldest data points are removed. Defaults to 200. """ super().__init__(self._create_ui_widget()) self.plot_height = plot_height @@ -157,7 +162,6 @@ def add_datapoint(self, y_coords: list[float]): """ for idx, y_coord in enumerate(y_coords): - if len(self._y_data[idx]) > self._max_data_points: self._y_data[idx] = self._y_data[idx][1:] @@ -307,8 +311,13 @@ def _build_single_plot(y_data: list[float], color: int, plot_idx: int): height=0, ) with omni.ui.Placer(offset_x=-20): + label_value = ( + self._y_max + - first_space * grid_resolution + - grid_line_idx * grid_resolution + ) omni.ui.Label( - f"{(self._y_max - first_space * grid_resolution - grid_line_idx * grid_resolution):.3f}", + f"{label_value:.3f}", width=8, height=8, alignment=omni.ui.Alignment.RIGHT_TOP, diff --git a/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py b/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py index a7cc31c070b7..0b20b10dabce 100644 --- a/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py +++ b/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py @@ -1,20 +1,21 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import numpy +import logging import weakref from dataclasses import MISSING from typing import TYPE_CHECKING +import numpy + import omni.kit.app -import omni.log -from isaacsim.core.api.simulation_context import SimulationContext from isaaclab.managers import ManagerBase +from isaaclab.sim import SimulationContext from isaaclab.utils import configclass from .image_plot import ImagePlot @@ -24,6 +25,9 @@ if TYPE_CHECKING: import omni.ui +# import logger +logger = logging.getLogger(__name__) + @configclass class ManagerLiveVisualizerCfg: @@ -57,7 +61,8 @@ def __init__(self, manager: ManagerBase, cfg: ManagerLiveVisualizerCfg = Manager """Initialize ManagerLiveVisualizer. Args: - manager: The manager with terms to be plotted. The manager must have a :meth:`get_active_iterable_terms` method. + manager: The manager with terms to be plotted. The manager must have a + :meth:`~isaaclab.managers.manager_base.ManagerBase.get_active_iterable_terms` method. cfg: The configuration file used to select desired manager terms to be plotted. """ @@ -79,7 +84,7 @@ def __init__(self, manager: ManagerBase, cfg: ManagerLiveVisualizerCfg = Manager if term_name in self._manager.active_terms: self.term_names.append(term_name) else: - omni.log.error( + logger.error( f"ManagerVisualizer Failure: ManagerTerm ({term_name}) does not exist in" f" Manager({self.cfg.manager_name})" ) @@ -94,17 +99,17 @@ def __init__(self, manager: ManagerBase, cfg: ManagerLiveVisualizerCfg = Manager if term_name in self._manager.active_terms[group]: self.term_names.append(f"{group}-{term_name}") else: - omni.log.error( + logger.error( f"ManagerVisualizer Failure: ManagerTerm ({term_name}) does not exist in" f" Group({group})" ) else: - omni.log.error( + logger.error( f"ManagerVisualizer Failure: Group ({group}) does not exist in" f" Manager({self.cfg.manager_name})" ) else: - omni.log.error( + logger.error( f"ManagerVisualizer Failure: Manager({self.cfg.manager_name}) does not utilize grouping of" " terms." ) @@ -148,7 +153,7 @@ def _set_env_selection_impl(self, env_idx: int): if env_idx > 0 and env_idx < self._manager.num_envs: self._env_idx = env_idx else: - omni.log.warn(f"Environment index is out of range (0, {self._manager.num_envs - 1})") + logger.warning(f"Environment index is out of range (0, {self._manager.num_envs - 1})") def _set_vis_frame_impl(self, frame: omni.ui.Frame): """Updates the assigned frame that can be used for visualizations. @@ -227,7 +232,7 @@ def _set_debug_vis_impl(self, debug_vis: bool): image = ImagePlot(image=numpy.array(term), label=name) self._term_visualizers.append(image) else: - omni.log.warn( + logger.warning( f"ManagerLiveVisualizer: Term ({name}) is not a supported data type for" " visualization." ) diff --git a/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py b/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py index 5c8700b66839..61a32119f300 100644 --- a/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py +++ b/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/ui/widgets/ui_widget_wrapper.py b/source/isaaclab/isaaclab/ui/widgets/ui_widget_wrapper.py index 8b43d9313671..9025a8c2e93f 100644 --- a/source/isaaclab/isaaclab/ui/widgets/ui_widget_wrapper.py +++ b/source/isaaclab/isaaclab/ui/widgets/ui_widget_wrapper.py @@ -1,9 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 file has been adapted from _isaac_sim/exts/isaacsim.gui.components/isaacsim/gui/components/element_wrappers/base_ui_element_wrappers.py +# This file has been adapted from: +# _isaac_sim/exts/isaacsim.gui.components/isaacsim/gui/components/element_wrappers/base_ui_element_wrappers.py from __future__ import annotations diff --git a/source/isaaclab/isaaclab/ui/xr_widgets/__init__.py b/source/isaaclab/isaaclab/ui/xr_widgets/__init__.py index 4375724f08f8..e0b341c8d2d5 100644 --- a/source/isaaclab/isaaclab/ui/xr_widgets/__init__.py +++ b/source/isaaclab/isaaclab/ui/xr_widgets/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py b/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py index ec084098dcb9..7d6fe00d7f6e 100644 --- a/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py +++ b/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +10,12 @@ import omni.kit.commands import omni.ui as ui -from isaacsim.core.utils.prims import delete_prim, get_prim_at_path from omni.kit.xr.scene_view.utils import UiContainer, WidgetComponent from omni.kit.xr.scene_view.utils.spatial_source import SpatialSource from pxr import Gf +import isaaclab.sim as sim_utils + Vec3Type: TypeAlias = Gf.Vec3f | Gf.Vec3d camera_facing_widget_container = {} @@ -34,7 +35,7 @@ def __init__( text: str | None = "Simple Text", style: dict[str, Any] | None = None, original_width: float = 0.0, - **kwargs + **kwargs, ): """Initialize the text widget. @@ -166,9 +167,11 @@ def show_instruction( container.root.clear() del camera_facing_widget_container[target_prim_path] + # Obtain stage handle + stage = sim_utils.get_current_stage() # Clean up existing widget - if get_prim_at_path(target_prim_path): - delete_prim(target_prim_path) + if stage.GetPrimAtPath(target_prim_path).IsValid(): + sim_utils.delete_prim(target_prim_path) width, height, wrapped_text = compute_widget_dimensions(text, font_size, max_width, min_width) @@ -193,10 +196,12 @@ def show_instruction( if copied_prim is not None: space_stack.append(SpatialSource.new_prim_path_source(target_prim_path)) - space_stack.extend([ - SpatialSource.new_translation_source(translation), - SpatialSource.new_look_at_camera_source(), - ]) + space_stack.extend( + [ + SpatialSource.new_translation_source(translation), + SpatialSource.new_look_at_camera_source(), + ] + ) # Create the UI container with the widget. container = UiContainer( diff --git a/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py b/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py index 2cac77b859bc..0be679a6929b 100644 --- a/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py +++ b/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,20 +7,24 @@ import contextlib import inspect -import numpy as np +import logging import threading import time -import torch from collections.abc import Callable from enum import Enum from typing import Any, Union -import omni.log +import numpy as np +import torch + from pxr import Gf from isaaclab.sim import SimulationContext from isaaclab.ui.xr_widgets import show_instruction +# import logger +logger = logging.getLogger(__name__) + class TriggerType(Enum): """Enumeration of trigger types for visualization callbacks. @@ -155,7 +159,7 @@ class VisualizationManager: # Type aliases for different callback signatures StandardCallback = Callable[["VisualizationManager", "DataCollector"], None] EventCallback = Callable[["VisualizationManager", "DataCollector", Any], None] - CallbackType = Union[StandardCallback, EventCallback] + CallbackType = Union[StandardCallback, EventCallback] # noqa: UP007 class TimeCountdown: """Internal class for managing periodic timer-based callbacks.""" @@ -307,9 +311,19 @@ def register_callback(self, trigger: TriggerType, arg: dict, callback: CallbackT - For TRIGGER_ON_EVENT: {"event_name": str} - For TRIGGER_ON_CHANGE: {"variable_name": str} - For TRIGGER_ON_UPDATE: {} - callback: Function to execute when trigger condition is met - - For TRIGGER_ON_EVENT: callback(manager: VisualizationManager, data_collector: DataCollector, event_params: Any) - - For others: callback(manager: VisualizationManager, data_collector: DataCollector) + callback: Function to execute when trigger condition is met. The callback should have + the following signatures according to the trigger type: + - For TRIGGER_ON_EVENT: + callback( + manager: VisualizationManager, + data_collector: DataCollector, + event_params: Any, + ) + - For others: + callback( + manager: VisualizationManager, + data_collector: DataCollector, + ) Raises: TypeError: If callback signature doesn't match the expected signature for the trigger type @@ -432,7 +446,7 @@ def _validate_callback_signature(self, trigger: TriggerType, callback: Callable) raise # If we can't inspect the signature (e.g., built-in functions), # just log a warning and proceed - omni.log.warn(f"Could not validate callback signature for {trigger.name}: {e}") + logger.warning(f"Could not validate callback signature for {trigger.name}: {e}") class XRVisualization: @@ -600,7 +614,7 @@ def assign_manager(cls, manager: type[VisualizationManager]) -> None: manager: Type of the visualization manager to assign """ if cls._instance is not None: - omni.log.error( + logger.error( f"Visualization system already initialized to {type(cls._instance._visualization_manager).__name__}," f" cannot assign manager {manager.__name__}" ) diff --git a/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py b/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py index eb424ae91916..3ba817c71190 100644 --- a/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py +++ b/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/__init__.py b/source/isaaclab/isaaclab/utils/__init__.py index a53659486999..1295715857f4 100644 --- a/source/isaaclab/isaaclab/utils/__init__.py +++ b/source/isaaclab/isaaclab/utils/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +10,8 @@ from .configclass import configclass from .dict import * from .interpolation import * +from .logger import * +from .mesh import * from .modifiers import * from .string import * from .timer import Timer diff --git a/source/isaaclab/isaaclab/utils/array.py b/source/isaaclab/isaaclab/utils/array.py index 4dd33f550934..d15fbc275dc7 100644 --- a/source/isaaclab/isaaclab/utils/array.py +++ b/source/isaaclab/isaaclab/utils/array.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,13 +8,13 @@ # needed to import for allowing type-hinting: torch.device | str | None from __future__ import annotations -import numpy as np -import torch from typing import Union +import numpy as np +import torch import warp as wp -TensorData = Union[np.ndarray, torch.Tensor, wp.array] +TensorData = Union[np.ndarray, torch.Tensor, wp.array] # noqa: UP007 """Type definition for a tensor data. Union of numpy, torch, and warp arrays. diff --git a/source/isaaclab/isaaclab/utils/assets.py b/source/isaaclab/isaaclab/utils/assets.py index ef61e9f89afd..22c5141587f6 100644 --- a/source/isaaclab/isaaclab/utils/assets.py +++ b/source/isaaclab/isaaclab/utils/assets.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +15,7 @@ import asyncio import io +import logging import os import tempfile import time @@ -23,6 +24,9 @@ import carb import omni.client +# import logger +logger = logging.getLogger(__name__) + NUCLEUS_ASSET_ROOT_DIR = carb.settings.get_settings().get("/persistent/isaac/asset_root/cloud") """Path to the root directory on the Nucleus Server.""" @@ -168,9 +172,9 @@ def check_usd_path_with_timeout(usd_path: str, timeout: float = 300, log_interva if now >= next_log_time: elapsed = int(now - start_time) if first_log: - omni.log.warn(f"Checking server availability for USD path: {usd_path} (timeout: {timeout}s)") + logger.warning(f"Checking server availability for USD path: {usd_path} (timeout: {timeout}s)") first_log = False - omni.log.warn(f"Waiting for server response... ({elapsed}s elapsed)") + logger.warning(f"Waiting for server response... ({elapsed}s elapsed)") next_log_time += log_interval loop.run_until_complete(asyncio.sleep(0.1)) # Yield to allow async work @@ -185,8 +189,9 @@ def check_usd_path_with_timeout(usd_path: str, timeout: float = 300, log_interva async def _is_usd_path_available(usd_path: str, timeout: float) -> bool: """Checks whether the given USD path is available on the Omniverse Nucleus server. - This function is a asynchronous routine to check the availability of the given USD path on the Omniverse Nucleus server. - It will return True if the USD path is available on the server, False otherwise. + This function is a asynchronous routine to check the availability of the given USD path on + the Omniverse Nucleus server. It will return True if the USD path is available on the server, + False otherwise. Args: usd_path: The remote or local USD file path to check. @@ -199,8 +204,8 @@ async def _is_usd_path_available(usd_path: str, timeout: float) -> bool: result, _ = await asyncio.wait_for(omni.client.stat_async(usd_path), timeout=timeout) return result == omni.client.Result.OK except asyncio.TimeoutError: - omni.log.warn(f"Timed out after {timeout}s while checking for USD: {usd_path}") + logger.warning(f"Timed out after {timeout}s while checking for USD: {usd_path}") return False except Exception as ex: - omni.log.warn(f"Exception during USD file check: {type(ex).__name__}: {ex}") + logger.warning(f"Exception during USD file check: {type(ex).__name__}: {ex}") return False diff --git a/source/isaaclab/isaaclab/utils/buffers/__init__.py b/source/isaaclab/isaaclab/utils/buffers/__init__.py index dc63468b8d55..64da4f6e6ae9 100644 --- a/source/isaaclab/isaaclab/utils/buffers/__init__.py +++ b/source/isaaclab/isaaclab/utils/buffers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/buffers/circular_buffer.py b/source/isaaclab/isaaclab/utils/buffers/circular_buffer.py index 8a01ba2a3700..c5c9fe9ff6ad 100644 --- a/source/isaaclab/isaaclab/utils/buffers/circular_buffer.py +++ b/source/isaaclab/isaaclab/utils/buffers/circular_buffer.py @@ -1,11 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from collections.abc import Sequence +import torch + class CircularBuffer: """Circular buffer for storing a history of batched tensor data. @@ -78,8 +79,11 @@ def current_length(self) -> torch.Tensor: @property def buffer(self) -> torch.Tensor: """Complete circular buffer with most recent entry at the end and oldest entry at the beginning. - Returns: - Complete circular buffer with most recent entry at the end and oldest entry at the beginning of dimension 1. The shape is [batch_size, max_length, data.shape[1:]]. + + The shape of the buffer is (batch_size, max_length, ...). + + Note: + The oldest entry is at the beginning of dimension 1. """ buf = self._buffer.clone() buf = torch.roll(buf, shifts=self.max_length - self._pointer - 1, dims=0) @@ -101,7 +105,8 @@ def reset(self, batch_ids: Sequence[int] | None = None): # reset the number of pushes for the specified batch indices self._num_pushes[batch_ids] = 0 if self._buffer is not None: - # set buffer at batch_id reset indices to 0.0 so that the buffer() getter returns the cleared circular buffer after reset. + # set buffer at batch_id reset indices to 0.0 so that the buffer() + # getter returns the cleared circular buffer after reset. self._buffer[:, batch_ids, :] = 0.0 def append(self, data: torch.Tensor): diff --git a/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py b/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py index 85332dd87c7d..dd1a1ef72684 100644 --- a/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py +++ b/source/isaaclab/isaaclab/utils/buffers/delay_buffer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,9 +6,10 @@ # needed because we concatenate int and torch.Tensor in the type hints from __future__ import annotations -import torch from collections.abc import Sequence +import torch + from .circular_buffer import CircularBuffer diff --git a/source/isaaclab/isaaclab/utils/buffers/timestamped_buffer.py b/source/isaaclab/isaaclab/utils/buffers/timestamped_buffer.py index 3c47fdfa8281..30b824464ad1 100644 --- a/source/isaaclab/isaaclab/utils/buffers/timestamped_buffer.py +++ b/source/isaaclab/isaaclab/utils/buffers/timestamped_buffer.py @@ -1,11 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from dataclasses import dataclass +import torch + @dataclass class TimestampedBuffer: diff --git a/source/isaaclab/isaaclab/utils/configclass.py b/source/isaaclab/isaaclab/utils/configclass.py index bce95d961c77..e46280a61ccf 100644 --- a/source/isaaclab/isaaclab/utils/configclass.py +++ b/source/isaaclab/isaaclab/utils/configclass.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -62,6 +62,7 @@ class EnvCfg: episode_length: int = 2000 viewer: ViewerCfg = ViewerCfg() + # create configuration instance env_cfg = EnvCfg(num_envs=24) @@ -153,6 +154,7 @@ class C: x: int y: int + c = C(1, 2) c1 = c.replace(x=3) assert c1.x == 3 and c1.y == 2 @@ -301,12 +303,13 @@ def _validate(obj: object, prefix: str = "") -> list[str]: def _process_mutable_types(cls): """Initialize all mutable elements through :obj:`dataclasses.Field` to avoid unnecessary complaints. - By default, dataclass requires usage of :obj:`field(default_factory=...)` to reinitialize mutable objects every time a new - class instance is created. If a member has a mutable type and it is created without specifying the `field(default_factory=...)`, - then Python throws an error requiring the usage of `default_factory`. + By default, dataclass requires usage of :obj:`field(default_factory=...)` to reinitialize mutable objects + every time a new class instance is created. If a member has a mutable type and it is created without + specifying the `field(default_factory=...)`, then Python throws an error requiring the usage of `default_factory`. - Additionally, Python only explicitly checks for field specification when the type is a list, set or dict. This misses the - use-case where the type is class itself. Thus, the code silently carries a bug with it which can lead to undesirable effects. + Additionally, Python only explicitly checks for field specification when the type is a list, set or dict. + This misses the use-case where the type is class itself. Thus, the code silently carries a bug with it which + can lead to undesirable effects. This function deals with this issue diff --git a/source/isaaclab/isaaclab/utils/datasets/__init__.py b/source/isaaclab/isaaclab/utils/datasets/__init__.py index a410fa0a443c..fce0fa308fa7 100644 --- a/source/isaaclab/isaaclab/utils/datasets/__init__.py +++ b/source/isaaclab/isaaclab/utils/datasets/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/datasets/dataset_file_handler_base.py b/source/isaaclab/isaaclab/utils/datasets/dataset_file_handler_base.py index dc953c0a3c69..201a0be370ec 100644 --- a/source/isaaclab/isaaclab/utils/datasets/dataset_file_handler_base.py +++ b/source/isaaclab/isaaclab/utils/datasets/dataset_file_handler_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/datasets/episode_data.py b/source/isaaclab/isaaclab/utils/datasets/episode_data.py index 31971b6181c6..55df8ebbcbe7 100644 --- a/source/isaaclab/isaaclab/utils/datasets/episode_data.py +++ b/source/isaaclab/isaaclab/utils/datasets/episode_data.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py b/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py index 2fa35ca1533a..46aeead2fd93 100644 --- a/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py +++ b/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,13 +8,14 @@ # # SPDX-License-Identifier: BSD-3-Clause -import h5py import json -import numpy as np import os -import torch from collections.abc import Iterable +import h5py +import numpy as np +import torch + from .dataset_file_handler_base import DatasetFileHandlerBase from .episode_data import EpisodeData @@ -136,18 +137,27 @@ def load_dataset_helper(group): return episode - def write_episode(self, episode: EpisodeData): + def write_episode(self, episode: EpisodeData, demo_id: int | None = None): """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. """ self._raise_if_not_initialized() if episode.is_empty(): return - # create episode group based on demo count - h5_episode_group = self._hdf5_data_group.create_group(f"demo_{self._demo_count}") + # Use custom demo id if provided, otherwise use default naming + if demo_id is not None: + episode_group_name = f"demo_{demo_id}" + else: + episode_group_name = f"demo_{self._demo_count}" + + # create episode group with the specified name + if episode_group_name in self._hdf5_data_group: + raise ValueError(f"Episode group '{episode_group_name}' already exists in the dataset") + h5_episode_group = self._hdf5_data_group.create_group(episode_group_name) # store number of steps taken if "actions" in episode.data: @@ -176,8 +186,10 @@ def create_dataset_helper(group, key, value): # increment total step counts self._hdf5_data_group.attrs["total"] += h5_episode_group.attrs["num_samples"] - # increment total demo counts - self._demo_count += 1 + # Only increment demo count if using default indexing + if demo_id is None: + # increment total demo counts + self._demo_count += 1 def flush(self): """Flush the episode data to disk.""" diff --git a/source/isaaclab/isaaclab/utils/dict.py b/source/isaaclab/isaaclab/utils/dict.py index 6e3a3d710465..de2062d66979 100644 --- a/source/isaaclab/isaaclab/utils/dict.py +++ b/source/isaaclab/isaaclab/utils/dict.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +8,11 @@ import collections.abc import hashlib import json -import torch from collections.abc import Iterable, Mapping, Sized from typing import Any +import torch + from .array import TENSOR_TYPE_CONVERSIONS, TENSOR_TYPES from .string import callable_to_string, string_to_callable, string_to_slice @@ -103,7 +104,6 @@ def update_class_from_dict(obj, data: dict[str, Any], _ns: str = "") -> None: # -- 2) iterable (list / tuple / etc.) --------------------- if isinstance(value, Iterable) and not isinstance(value, str): - # ---- 2a) flat iterable → replace wholesale ---------- if all(not isinstance(el, Mapping) for el in value): out_val = tuple(value) if isinstance(obj_mem, tuple) else value diff --git a/source/isaaclab/isaaclab/utils/interpolation/__init__.py b/source/isaaclab/isaaclab/utils/interpolation/__init__.py index d6d1e6ec134e..25f6be5f0014 100644 --- a/source/isaaclab/isaaclab/utils/interpolation/__init__.py +++ b/source/isaaclab/isaaclab/utils/interpolation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/interpolation/linear_interpolation.py b/source/isaaclab/isaaclab/utils/interpolation/linear_interpolation.py index cb050c3051f5..84a371280427 100644 --- a/source/isaaclab/isaaclab/utils/interpolation/linear_interpolation.py +++ b/source/isaaclab/isaaclab/utils/interpolation/linear_interpolation.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/io/__init__.py b/source/isaaclab/isaaclab/utils/io/__init__.py index d2e038312316..9a8b16ed157a 100644 --- a/source/isaaclab/isaaclab/utils/io/__init__.py +++ b/source/isaaclab/isaaclab/utils/io/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/io/torchscript.py b/source/isaaclab/isaaclab/utils/io/torchscript.py index df5fe454bf32..df96ebca1233 100644 --- a/source/isaaclab/isaaclab/utils/io/torchscript.py +++ b/source/isaaclab/isaaclab/utils/io/torchscript.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +7,7 @@ """TorchScript I/O utilities.""" import os + import torch diff --git a/source/isaaclab/isaaclab/utils/io/yaml.py b/source/isaaclab/isaaclab/utils/io/yaml.py index 49fe1e079267..0f2dbeeefb9c 100644 --- a/source/isaaclab/isaaclab/utils/io/yaml.py +++ b/source/isaaclab/isaaclab/utils/io/yaml.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,6 +6,7 @@ """Utilities for file I/O with yaml.""" import os + import yaml from isaaclab.utils import class_to_dict diff --git a/source/isaaclab/isaaclab/utils/logger.py b/source/isaaclab/isaaclab/utils/logger.py new file mode 100644 index 000000000000..c9293e931a72 --- /dev/null +++ b/source/isaaclab/isaaclab/utils/logger.py @@ -0,0 +1,182 @@ +# 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 + +"""Sub-module with logging utilities. + +To use the logger, you can use the :func:`logging.getLogger` function. + +Example: + >>> import logging + >>> + >>> # define logger for the current module (enables fine-control) + >>> logger = logging.getLogger(__name__) + >>> + >>> # log messages + >>> logger.info("This is an info message") + >>> logger.warning("This is a warning message") + >>> logger.error("This is an error message") + >>> logger.critical("This is a critical message") + >>> logger.debug("This is a debug message") +""" + +from __future__ import annotations + +import logging +import os +import sys +import tempfile +import time +from datetime import datetime +from typing import Literal + + +def configure_logging( + logging_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "WARNING", + save_logs_to_file: bool = True, + log_dir: str | None = None, +) -> logging.Logger: + """Setup the logger with a colored formatter and a rate limit filter. + + This function defines the default logger for IsaacLab. It adds a stream handler with a colored formatter + and a rate limit filter. If :attr:`save_logs_to_file` is True, it also adds a file handler to save the logs + to a file. The log directory can be specified using :attr:`log_dir`. If not provided, the logs will be saved + to the temp directory with the sub-directory "isaaclab/logs". + + The log file name is formatted as "isaaclab_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log". + The log record format is "%(asctime)s [%(filename)s:%(lineno)d] %(levelname)s: %(message)s". + The date format is "%Y-%m-%d %H:%M:%S". + + Args: + logging_level: The logging level. + save_logs_to_file: Whether to save the logs to a file. + log_dir: The directory to save the logs to. Default is None, in which case the logs + will be saved to the temp directory with the sub-directory "isaaclab/logs". + + Returns: + The root logger. + """ + root_logger = logging.getLogger() + # the root logger must be the lowest level to ensure that all messages are logged + root_logger.setLevel(logging.DEBUG) + + # remove existing handlers + # Note: iterate over a copy [:] to avoid modifying list during iteration + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # add a stream handler with default level + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging_level) + + # add a colored formatter + formatter = ColoredFormatter(fmt="%(asctime)s [%(filename)s] %(levelname)s: %(message)s", datefmt="%H:%M:%S") + handler.setFormatter(formatter) + handler.addFilter(RateLimitFilter(interval_seconds=5)) + root_logger.addHandler(handler) + + # add a file handler + if save_logs_to_file: + # if log_dir is not provided, use the temp directory + if log_dir is None: + log_dir = os.path.join(tempfile.gettempdir(), "isaaclab", "logs") + # create the log directory if it does not exist + os.makedirs(log_dir, exist_ok=True) + # create the log file path + log_file_path = os.path.join(log_dir, f"isaaclab_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log") + + # create the file handler + file_handler = logging.FileHandler(log_file_path, mode="w", encoding="utf-8") + file_handler.setLevel(logging.DEBUG) + file_formatter = logging.Formatter( + fmt="%(asctime)s [%(filename)s:%(lineno)d] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" + ) + file_handler.setFormatter(file_formatter) + root_logger.addHandler(file_handler) + + # print the log file path once at startup with nice formatting + cyan = "\033[36m" # cyan color + bold = "\033[1m" # bold text + reset = "\033[0m" # reset formatting + message = f"[INFO][IsaacLab]: Logging to file: {log_file_path}" + border = "=" * len(message) + print(f"\n{cyan}{border}{reset}") + print(f"{cyan}{bold}{message}{reset}") + print(f"{cyan}{border}{reset}\n") + + # return the root logger + return root_logger + + +class ColoredFormatter(logging.Formatter): + """Colored formatter for logging. + + This formatter colors the log messages based on the log level. + """ + + COLORS = { + "WARNING": "\033[33m", # orange/yellow + "ERROR": "\033[31m", # red + "CRITICAL": "\033[1;31m", # bold red + "INFO": "\033[0m", # reset + "DEBUG": "\033[0m", + } + """Colors for different log levels.""" + + RESET = "\033[0m" + """Reset color.""" + + def format(self, record: logging.LogRecord) -> str: + """Format the log record. + + Args: + record: The log record to format. + + Returns: + The formatted log record. + """ + color = self.COLORS.get(record.levelname, self.RESET) + message = super().format(record) + return f"{color}{message}{self.RESET}" + + +class RateLimitFilter(logging.Filter): + """Custom rate-limited warning filter. + + This filter allows warning-level messages only once every few seconds per message. + This is useful to avoid flooding the log with the same message multiple times. + """ + + def __init__(self, interval_seconds: int = 5): + """Initialize the rate limit filter. + + Args: + interval_seconds: The interval in seconds to limit the warnings. + Defaults to 5 seconds. + """ + super().__init__() + self.interval = interval_seconds + self.last_emitted = {} + + def filter(self, record: logging.LogRecord) -> bool: + """Allow warning-level messages only once every few seconds per message. + + Args: + record: The log record to filter. + + Returns: + True if the message should be logged, False otherwise. + """ + # only filter warning-level messages + if record.levelno != logging.WARNING: + return True + # check if the message has been logged in the last interval + now = time.time() + msg_key = record.getMessage() + if msg_key not in self.last_emitted or (now - self.last_emitted[msg_key]) > self.interval: + # if the message has not been logged in the last interval, log it + self.last_emitted[msg_key] = now + return True + # if the message has been logged in the last interval, do not log it + return False diff --git a/source/isaaclab/isaaclab/utils/math.py b/source/isaaclab/isaaclab/utils/math.py index f8ad612a9164..96314abfbd09 100644 --- a/source/isaaclab/isaaclab/utils/math.py +++ b/source/isaaclab/isaaclab/utils/math.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,13 +8,16 @@ # needed to import for allowing type-hinting: torch.Tensor | np.ndarray from __future__ import annotations +import logging import math +from typing import Literal + import numpy as np import torch import torch.nn.functional -from typing import Literal -import omni.log +# import logger +logger = logging.getLogger(__name__) """ General @@ -694,7 +697,7 @@ def quat_rotate(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: The rotated vector in (x, y, z). Shape is (..., 3). """ # deprecation - omni.log.warn( + logger.warning( "The function 'quat_rotate' will be deprecated in favor of the faster method 'quat_apply'." " Please use 'quat_apply' instead...." ) @@ -705,7 +708,8 @@ def quat_rotate_inverse(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: """Rotate a vector by the inverse of a quaternion along the last dimension of q and v. .. deprecated v2.1.0: - This function will be removed in a future release in favor of the faster implementation :meth:`quat_apply_inverse`. + This function will be removed in a future release in favor of the faster implementation + :meth:`quat_apply_inverse`. Args: q: The quaternion in (w, x, y, z). Shape is (..., 4). @@ -714,7 +718,7 @@ def quat_rotate_inverse(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: Returns: The rotated vector in (x, y, z). Shape is (..., 3). """ - omni.log.warn( + logger.warning( "The function 'quat_rotate_inverse' will be deprecated in favor of the faster method 'quat_apply_inverse'." " Please use 'quat_apply_inverse' instead...." ) @@ -1524,7 +1528,10 @@ def convert_camera_frame_orientation_convention( .. math:: - T_{ROS} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & -1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} + T_{ROS} = + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ 0 & -1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 + \end{bmatrix} T_{USD} On the other hand, the typical world coordinate system is with +X pointing forward, +Y pointing left, and +Z pointing up. The camera can also be set in this convention by rotating the camera by :math:`90^{\circ}` @@ -1532,7 +1539,10 @@ def convert_camera_frame_orientation_convention( .. math:: - T_{WORLD} = \begin{bmatrix} 0 & 0 & -1 & 0 \\ -1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} + T_{WORLD} = + \begin{bmatrix} + 0 & 0 & -1 & 0 \\ -1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 + \end{bmatrix} T_{USD} Thus, based on their application, cameras follow different conventions for their orientation. This function converts a quaternion from one convention to another. @@ -1701,7 +1711,8 @@ def pose_inv(pose: torch.Tensor) -> torch.Tensor: # Take transpose of last 2 dimensions inv_pose[..., :3, :3] = pose[..., :3, :3].transpose(-1, -2) - # note: PyTorch matmul wants shapes [..., 3, 3] x [..., 3, 1] -> [..., 3, 1] so we add a dimension and take it away after + # note: PyTorch matmul wants shapes [..., 3, 3] x [..., 3, 1] -> [..., 3, 1] + # so we add a dimension and take it away after inv_pose[..., :3, 3] = torch.matmul(-inv_pose[..., :3, :3], pose[..., :3, 3:4])[..., 0] inv_pose[..., 3, 3] = 1.0 return inv_pose diff --git a/source/isaaclab/isaaclab/utils/mesh.py b/source/isaaclab/isaaclab/utils/mesh.py new file mode 100644 index 000000000000..9e6315cc83c7 --- /dev/null +++ b/source/isaaclab/isaaclab/utils/mesh.py @@ -0,0 +1,183 @@ +# 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 + + +"""Utility functions for working with meshes.""" + +from collections.abc import Callable + +import numpy as np +import trimesh + +from pxr import Usd, UsdGeom + +__all__ = [ + "create_trimesh_from_geom_mesh", + "create_trimesh_from_geom_shape", + "convert_faces_to_triangles", + "PRIMITIVE_MESH_TYPES", +] + + +def create_trimesh_from_geom_mesh(mesh_prim: Usd.Prim) -> trimesh.Trimesh: + """Reads the vertices and faces of a mesh prim. + + The function reads the vertices and faces of a mesh prim and returns it. If the underlying mesh is a quad mesh, + it converts it to a triangle mesh. + + Args: + mesh_prim: The mesh prim to read the vertices and faces from. + + Returns: + A trimesh.Trimesh object containing the mesh geometry. + """ + if mesh_prim.GetTypeName() != "Mesh": + raise ValueError(f"Prim at path '{mesh_prim.GetPath()}' is not a mesh.") + # cast into UsdGeomMesh + mesh = UsdGeom.Mesh(mesh_prim) + + # read the vertices and faces + points = np.asarray(mesh.GetPointsAttr().Get()).copy() + + # Load faces and convert to triangle if needed. (Default is quads) + num_vertex_per_face = np.asarray(mesh.GetFaceVertexCountsAttr().Get()) + indices = np.asarray(mesh.GetFaceVertexIndicesAttr().Get()) + return trimesh.Trimesh(points, convert_faces_to_triangles(indices, num_vertex_per_face)) + + +def create_trimesh_from_geom_shape(prim: Usd.Prim) -> trimesh.Trimesh: + """Converts a primitive object to a trimesh. + + Args: + prim: The prim that should be converted to a trimesh. + + Returns: + A trimesh object representing the primitive. + + Raises: + ValueError: If the prim is not a supported primitive. Check PRIMITIVE_MESH_TYPES for supported primitives. + """ + + if prim.GetTypeName() not in PRIMITIVE_MESH_TYPES: + raise ValueError(f"Prim at path '{prim.GetPath()}' is not a primitive mesh. Cannot convert to trimesh.") + + return _MESH_CONVERTERS_CALLBACKS[prim.GetTypeName()](prim) + + +def convert_faces_to_triangles(faces: np.ndarray, point_counts: np.ndarray) -> np.ndarray: + """Converts quad mesh face indices into triangle face indices. + + This function expects an array of faces (indices) and the number of points per face. It then converts potential + quads into triangles and returns the new triangle face indices as a numpy array of shape (n_faces_new, 3). + + Args: + faces: The faces of the quad mesh as a one-dimensional array. Shape is (N,). + point_counts: The number of points per face. Shape is (N,). + + Returns: + The new face ids with triangles. Shape is (n_faces_new, 3). + """ + # check if the mesh is already triangulated + if (point_counts == 3).all(): + return faces.reshape(-1, 3) # already triangulated + all_faces = [] + + vertex_counter = 0 + # Iterates over all faces of the mesh to triangulate them. + # could be very slow for large meshes + for num_points in point_counts: + # Triangulate n-gons (n>4) using fan triangulation + for i in range(num_points - 2): + triangle = np.array([faces[vertex_counter], faces[vertex_counter + 1 + i], faces[vertex_counter + 2 + i]]) + all_faces.append(triangle) + + vertex_counter += num_points + return np.asarray(all_faces) + + +""" +Internal USD Shape Handlers. +""" + + +def _create_plane_trimesh(prim: Usd.Prim) -> trimesh.Trimesh: + """Creates a trimesh for a plane primitive.""" + size = (2e6, 2e6) + vertices = np.array([[size[0], size[1], 0], [size[0], 0.0, 0], [0.0, size[1], 0], [0.0, 0.0, 0]]) - np.array( + [size[0] / 2.0, size[1] / 2.0, 0.0] + ) + faces = np.array([[1, 0, 2], [2, 3, 1]]) + return trimesh.Trimesh(vertices=vertices, faces=faces) + + +def _create_cube_trimesh(prim: Usd.Prim) -> trimesh.Trimesh: + """Creates a trimesh for a cube primitive.""" + size = prim.GetAttribute("size").Get() + extends = [size, size, size] + return trimesh.creation.box(extends) + + +def _create_sphere_trimesh(prim: Usd.Prim, subdivisions: int = 2) -> trimesh.Trimesh: + """Creates a trimesh for a sphere primitive.""" + radius = prim.GetAttribute("radius").Get() + mesh = trimesh.creation.icosphere(radius=radius, subdivisions=subdivisions) + return mesh + + +def _create_cylinder_trimesh(prim: Usd.Prim) -> trimesh.Trimesh: + """Creates a trimesh for a cylinder primitive.""" + radius = prim.GetAttribute("radius").Get() + height = prim.GetAttribute("height").Get() + mesh = trimesh.creation.cylinder(radius=radius, height=height) + axis = prim.GetAttribute("axis").Get() + if axis == "X": + # rotate −90° about Y to point the length along +X + R = trimesh.transformations.rotation_matrix(np.radians(-90), [0, 1, 0]) + mesh.apply_transform(R) + elif axis == "Y": + # rotate +90° about X to point the length along +Y + R = trimesh.transformations.rotation_matrix(np.radians(90), [1, 0, 0]) + mesh.apply_transform(R) + return mesh + + +def _create_capsule_trimesh(prim: Usd.Prim) -> trimesh.Trimesh: + """Creates a trimesh for a capsule primitive.""" + radius = prim.GetAttribute("radius").Get() + height = prim.GetAttribute("height").Get() + mesh = trimesh.creation.capsule(radius=radius, height=height) + axis = prim.GetAttribute("axis").Get() + if axis == "X": + # rotate −90° about Y to point the length along +X + R = trimesh.transformations.rotation_matrix(np.radians(-90), [0, 1, 0]) + mesh.apply_transform(R) + elif axis == "Y": + # rotate +90° about X to point the length along +Y + R = trimesh.transformations.rotation_matrix(np.radians(90), [1, 0, 0]) + mesh.apply_transform(R) + return mesh + + +def _create_cone_trimesh(prim: Usd.Prim) -> trimesh.Trimesh: + """Creates a trimesh for a cone primitive.""" + radius = prim.GetAttribute("radius").Get() + height = prim.GetAttribute("height").Get() + mesh = trimesh.creation.cone(radius=radius, height=height) + # shift all vertices down by height/2 for usd / trimesh cone primitive definition discrepancy + mesh.apply_translation((0.0, 0.0, -height / 2.0)) + return mesh + + +_MESH_CONVERTERS_CALLBACKS: dict[str, Callable[[Usd.Prim], trimesh.Trimesh]] = { + "Plane": _create_plane_trimesh, + "Cube": _create_cube_trimesh, + "Sphere": _create_sphere_trimesh, + "Cylinder": _create_cylinder_trimesh, + "Capsule": _create_capsule_trimesh, + "Cone": _create_cone_trimesh, +} + +PRIMITIVE_MESH_TYPES = list(_MESH_CONVERTERS_CALLBACKS.keys()) +"""List of supported primitive mesh types that can be converted to a trimesh.""" diff --git a/source/isaaclab/isaaclab/utils/modifiers/__init__.py b/source/isaaclab/isaaclab/utils/modifiers/__init__.py index 310f7d43efc8..b79a5140a7a8 100644 --- a/source/isaaclab/isaaclab/utils/modifiers/__init__.py +++ b/source/isaaclab/isaaclab/utils/modifiers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/modifiers/modifier.py b/source/isaaclab/isaaclab/utils/modifiers/modifier.py index 6121d69ed1f8..182a606565ad 100644 --- a/source/isaaclab/isaaclab/utils/modifiers/modifier.py +++ b/source/isaaclab/isaaclab/utils/modifiers/modifier.py @@ -1,14 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + from .modifier_base import ModifierBase if TYPE_CHECKING: diff --git a/source/isaaclab/isaaclab/utils/modifiers/modifier_base.py b/source/isaaclab/isaaclab/utils/modifiers/modifier_base.py index 0a01858cbd2d..65a7fe0bb8a2 100644 --- a/source/isaaclab/isaaclab/utils/modifiers/modifier_base.py +++ b/source/isaaclab/isaaclab/utils/modifiers/modifier_base.py @@ -1,15 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from abc import ABC, abstractmethod from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + if TYPE_CHECKING: from .modifier_cfg import ModifierCfg @@ -31,7 +32,7 @@ class ModifierBase(ABC): from isaaclab.utils import modifiers # define custom keyword arguments to pass to ModifierCfg - kwarg_dict = {"arg_1" : VAL_1, "arg_2" : VAL_2} + kwarg_dict = {"arg_1": VAL_1, "arg_2": VAL_2} # create modifier configuration object # func is the class name of the modifier and params is the dictionary of arguments diff --git a/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py b/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py index e80a6cab81e3..cf018fc07165 100644 --- a/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py +++ b/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch from collections.abc import Callable from dataclasses import MISSING from typing import Any +import torch + from isaaclab.utils import configclass from . import modifier diff --git a/source/isaaclab/isaaclab/utils/noise/__init__.py b/source/isaaclab/isaaclab/utils/noise/__init__.py index d2f703758b05..7f91067fd005 100644 --- a/source/isaaclab/isaaclab/utils/noise/__init__.py +++ b/source/isaaclab/isaaclab/utils/noise/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/noise/noise_cfg.py b/source/isaaclab/isaaclab/utils/noise/noise_cfg.py index 0c49828b3ff6..b3275643fd29 100644 --- a/source/isaaclab/isaaclab/utils/noise/noise_cfg.py +++ b/source/isaaclab/isaaclab/utils/noise/noise_cfg.py @@ -1,15 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Callable from dataclasses import MISSING from typing import Literal +import torch + from isaaclab.utils import configclass from . import noise_model diff --git a/source/isaaclab/isaaclab/utils/noise/noise_model.py b/source/isaaclab/isaaclab/utils/noise/noise_model.py index dae36b55c722..78b93c9f099b 100644 --- a/source/isaaclab/isaaclab/utils/noise/noise_model.py +++ b/source/isaaclab/isaaclab/utils/noise/noise_model.py @@ -1,14 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + if TYPE_CHECKING: from . import noise_cfg diff --git a/source/isaaclab/isaaclab/utils/seed.py b/source/isaaclab/isaaclab/utils/seed.py new file mode 100644 index 000000000000..6b2a8ff97adf --- /dev/null +++ b/source/isaaclab/isaaclab/utils/seed.py @@ -0,0 +1,45 @@ +# 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 os +import random + +import numpy as np +import torch +import warp as wp + + +def configure_seed(seed: int | None, torch_deterministic: bool = False) -> int: + """Set seed across all random number generators (torch, numpy, random, warp). + + Args: + seed: The random seed value. If None, generates a random seed. + torch_deterministic: If True, enables deterministic mode for torch operations. + + Returns: + The seed value that was set. + """ + if seed is None or seed == -1: + seed = 42 if torch_deterministic else random.randint(0, 10000) + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + wp.rand_init(seed) + + if torch_deterministic: + # refer to https://docs.nvidia.com/cuda/cublas/index.html#cublasApi_reproducibility + os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8" + torch.backends.cudnn.benchmark = False + torch.backends.cudnn.deterministic = True + torch.use_deterministic_algorithms(True) + else: + torch.backends.cudnn.benchmark = True + torch.backends.cudnn.deterministic = False + + return seed diff --git a/source/isaaclab/isaaclab/utils/sensors.py b/source/isaaclab/isaaclab/utils/sensors.py index 92d603fcd8a2..d9016c2f885a 100644 --- a/source/isaaclab/isaaclab/utils/sensors.py +++ b/source/isaaclab/isaaclab/utils/sensors.py @@ -1,9 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 omni +import logging + +# import logger +logger = logging.getLogger(__name__) def convert_camera_intrinsics_to_usd( @@ -16,15 +19,13 @@ def convert_camera_intrinsics_to_usd( The matrix is defined as [f_x, 0, c_x, 0, f_y, c_y, 0, 0, 1]. Shape is (9,). width: Width of the image (in pixels). height: Height of the image (in pixels). - focal_length: Perspective focal length (in cm) used to calculate pixel size. Defaults to None. If None - focal_length will be calculated 1 / width. + focal_length: Perspective focal length (in cm) used to calculate pixel size. Defaults to None, + in which case, the focal length will be calculated as 1 / width. Returns: A dictionary of USD camera parameters for focal_length, horizontal_aperture, vertical_aperture, - horizontal_aperture_offset, and vertical_aperture_offset. + horizontal_aperture_offset, and vertical_aperture_offset. """ - usd_params = dict - # extract parameters from matrix f_x = intrinsic_matrix[0] f_y = intrinsic_matrix[4] @@ -33,11 +34,11 @@ def convert_camera_intrinsics_to_usd( # warn about non-square pixels if abs(f_x - f_y) > 1e-4: - omni.log.warn("Camera non square pixels are not supported by Omniverse. The average of f_x and f_y are used.") + logger.warning("Camera non square pixels are not supported by Omniverse. The average of f_x and f_y are used.") # warn about aperture offsets if abs((c_x - float(width) / 2) > 1e-4 or (c_y - float(height) / 2) > 1e-4): - omni.log.warn( + logger.warning( "Camera aperture offsets are not supported by Omniverse. c_x and c_y will be half of width and height" ) diff --git a/source/isaaclab/isaaclab/utils/string.py b/source/isaaclab/isaaclab/utils/string.py index 43a2fa0b3106..dc1cdaf53477 100644 --- a/source/isaaclab/isaaclab/utils/string.py +++ b/source/isaaclab/isaaclab/utils/string.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -290,7 +290,8 @@ def resolve_matching_names_values( For example, consider the dictionary is {"a|d|e": 1, "b|c": 2}, the list of strings is ['a', 'b', 'c', 'd', 'e']. If :attr:`preserve_order` is False, then the function will return the indices of the matched strings, the matched strings, and the values as: ([0, 1, 2, 3, 4], ['a', 'b', 'c', 'd', 'e'], [1, 2, 2, 1, 1]). When - :attr:`preserve_order` is True, it will return them as: ([0, 3, 4, 1, 2], ['a', 'd', 'e', 'b', 'c'], [1, 1, 1, 2, 2]). + :attr:`preserve_order` is True, it will return them as: + ([0, 3, 4, 1, 2], ['a', 'd', 'e', 'b', 'c'], [1, 1, 1, 2, 2]). Args: data: A dictionary of regular expressions and values to match the strings in the list. @@ -370,3 +371,46 @@ def resolve_matching_names_values( ) # return return index_list, names_list, values_list + + +def find_unique_string_name(initial_name: str, is_unique_fn: Callable[[str], bool]) -> str: + """Find a unique string name based on the predicate function provided. + The string is appended with "_N", where N is a natural number till the resultant string + is unique. + Args: + initial_name (str): The initial string name. + is_unique_fn (Callable[[str], bool]): The predicate function to validate against. + Returns: + str: A unique string based on input function. + """ + if is_unique_fn(initial_name): + return initial_name + iterator = 1 + result = initial_name + "_" + str(iterator) + while not is_unique_fn(result): + result = initial_name + "_" + str(iterator) + iterator += 1 + return result + + +def find_root_prim_path_from_regex(prim_path_regex: str) -> tuple[str, int]: + """Find the first prim above the regex pattern prim and its position. + Args: + prim_path_regex (str): full prim path including the regex pattern prim. + Returns: + Tuple[str, int]: First position is the prim path to the parent of the regex prim. + Second position represents the level of the regex prim in the USD stage tree representation. + """ + prim_paths_list = str(prim_path_regex).split("/") + root_idx = None + for prim_path_idx in range(len(prim_paths_list)): + chars = set("[]*|^") + if any((c in chars) for c in prim_paths_list[prim_path_idx]): + root_idx = prim_path_idx + break + root_prim_path = None + tree_level = None + if root_idx is not None: + root_prim_path = "/".join(prim_paths_list[:root_idx]) + tree_level = root_idx + return root_prim_path, tree_level diff --git a/source/isaaclab/isaaclab/utils/timer.py b/source/isaaclab/isaaclab/utils/timer.py index fa09f4903393..4d9951db60c8 100644 --- a/source/isaaclab/isaaclab/utils/timer.py +++ b/source/isaaclab/isaaclab/utils/timer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/isaaclab/utils/types.py b/source/isaaclab/isaaclab/utils/types.py index aa6f1fbfd454..321c361792a4 100644 --- a/source/isaaclab/isaaclab/utils/types.py +++ b/source/isaaclab/isaaclab/utils/types.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +7,11 @@ from __future__ import annotations -import torch from collections.abc import Sequence from dataclasses import dataclass +import torch + @dataclass class ArticulationActions: diff --git a/source/isaaclab/isaaclab/utils/version.py b/source/isaaclab/isaaclab/utils/version.py index 0371d8e730d3..358a5550aa1c 100644 --- a/source/isaaclab/isaaclab/utils/version.py +++ b/source/isaaclab/isaaclab/utils/version.py @@ -1,23 +1,94 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 -"""Utility function for version comparison.""" +"""Utility functions for versioning.""" + +from __future__ import annotations + +import functools + +from packaging.version import Version + + +@functools.lru_cache(maxsize=1) +def get_isaac_sim_version() -> Version: + """Get the Isaac Sim version as a Version object, cached for performance. + + This function wraps :func:`isaacsim.core.version.get_version()` and caches the result + to avoid repeated file I/O operations. The underlying Isaac Sim function reads from + a file each time it's called, which can be slow when called frequently. + + Returns: + A :class:`packaging.version.Version` object representing the Isaac Sim version. + This object supports rich comparison operators (<, <=, >, >=, ==, !=). + + Example: + >>> from isaaclab.utils import get_isaac_sim_version + >>> from packaging.version import Version + >>> + >>> isaac_version = get_isaac_sim_version() + >>> print(isaac_version) + 5.0.0 + >>> + >>> # Natural version comparisons + >>> if isaac_version >= Version("5.0.0"): + ... print("Using Isaac Sim 5.0 or later") + >>> + >>> # Access components + >>> print(isaac_version.major, isaac_version.minor, isaac_version.micro) + 5 0 0 + """ + from isaacsim.core.version import get_version + + version_tuple = get_version() + # version_tuple[2] = major (year), [3] = minor (release), [4] = micro (patch) + return Version(f"{version_tuple[2]}.{version_tuple[3]}.{version_tuple[4]}") def compare_versions(v1: str, v2: str) -> int: - parts1 = list(map(int, v1.split("."))) - parts2 = list(map(int, v2.split("."))) - - # Pad the shorter version with zeros (e.g. 1.2 vs 1.2.0) - length = max(len(parts1), len(parts2)) - parts1 += [0] * (length - len(parts1)) - parts2 += [0] * (length - len(parts2)) - - if parts1 > parts2: - return 1 # v1 is greater - elif parts1 < parts2: - return -1 # v2 is greater + """Compare two version strings and return the comparison result. + + The version strings are expected to be in the format "x.y.z" where x, y, + and z are integers. The version strings are compared lexicographically. + + .. note:: + This function is provided for backward compatibility. For new code, + prefer using :class:`packaging.version.Version` objects directly with + comparison operators (``<``, ``<=``, ``>``, ``>=``, ``==``, ``!=``). + + Args: + v1: The first version string. + v2: The second version string. + + Returns: + An integer indicating the comparison result: + + - :attr:`1` if v1 is greater + - :attr:`-1` if v2 is greater + - :attr:`0` if v1 and v2 are equal + + Example: + >>> from isaaclab.utils import compare_versions + >>> compare_versions("5.0.0", "4.5.0") + 1 + >>> compare_versions("4.5.0", "5.0.0") + -1 + >>> compare_versions("5.0.0", "5.0.0") + 0 + >>> + >>> # Better: use Version objects directly + >>> from packaging.version import Version + >>> Version("5.0.0") > Version("4.5.0") + True + """ + ver1 = Version(v1) + ver2 = Version(v2) + + if ver1 > ver2: + return 1 + elif ver1 < ver2: + return -1 else: - return 0 # versions are equal + return 0 diff --git a/source/isaaclab/isaaclab/utils/warp/__init__.py b/source/isaaclab/isaaclab/utils/warp/__init__.py index 14c49f25528d..12c9a20f8bb3 100644 --- a/source/isaaclab/isaaclab/utils/warp/__init__.py +++ b/source/isaaclab/isaaclab/utils/warp/__init__.py @@ -1,8 +1,9 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Sub-module containing operations based on warp.""" -from .ops import convert_to_warp_mesh, raycast_mesh +from . import fabric # noqa: F401 +from .ops import convert_to_warp_mesh, raycast_dynamic_meshes, raycast_mesh, raycast_single_mesh diff --git a/source/isaaclab/isaaclab/utils/warp/fabric.py b/source/isaaclab/isaaclab/utils/warp/fabric.py new file mode 100644 index 000000000000..3fc42ff94236 --- /dev/null +++ b/source/isaaclab/isaaclab/utils/warp/fabric.py @@ -0,0 +1,207 @@ +# 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 + +# pyright: ignore +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # noqa: E501 +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Warp kernels for GPU-accelerated Fabric operations.""" + +from typing import TYPE_CHECKING, Any + +import warp as wp + +if TYPE_CHECKING: + FabricArrayUInt32 = Any + FabricArrayMat44d = Any + ArrayUInt32 = Any + ArrayUInt32_1d = Any + ArrayFloat32_2d = Any +else: + FabricArrayUInt32 = wp.fabricarray(dtype=wp.uint32) + FabricArrayMat44d = wp.fabricarray(dtype=wp.mat44d) + ArrayUInt32 = wp.array(ndim=1, dtype=wp.uint32) + ArrayUInt32_1d = wp.array(dtype=wp.uint32) + ArrayFloat32_2d = wp.array(ndim=2, dtype=wp.float32) + + +@wp.kernel(enable_backward=False) +def set_view_to_fabric_array(fabric_to_view: FabricArrayUInt32, view_to_fabric: ArrayUInt32): + """Create bidirectional mapping from view indices to fabric indices.""" + fabric_idx = int(wp.tid()) + view_idx = int(fabric_to_view[fabric_idx]) + view_to_fabric[view_idx] = wp.uint32(fabric_idx) + + +@wp.kernel(enable_backward=False) +def arange_k(a: ArrayUInt32_1d): + """Fill array with sequential indices.""" + tid = int(wp.tid()) + a[tid] = wp.uint32(tid) + + +@wp.kernel(enable_backward=False) +def decompose_fabric_transformation_matrix_to_warp_arrays( + fabric_matrices: FabricArrayMat44d, + array_positions: ArrayFloat32_2d, + array_orientations: ArrayFloat32_2d, + array_scales: ArrayFloat32_2d, + indices: ArrayUInt32, + mapping: ArrayUInt32, +): + """Decompose Fabric transformation matrices into position, orientation, and scale arrays. + + This kernel extracts transform components from Fabric's omni:fabric:worldMatrix attribute + and stores them in separate arrays. It handles the quaternion convention conversion + (Warp uses xyzw, Isaac Lab uses wxyz). + + Args: + fabric_matrices: Fabric array containing 4x4 transformation matrices + array_positions: Output array for positions (N, 3) + array_orientations: Output array for quaternions in wxyz format (N, 4) + array_scales: Output array for scales (N, 3) + indices: View indices to process + mapping: Mapping from view indices to fabric indices + """ + # Thread index is the output array index (0, 1, 2, ... for N elements) + output_index = wp.tid() + # View index is which prim in the view we're reading from (e.g., 0, 2, 4 from indices=[0,2,4]) + view_index = indices[output_index] + # Fabric index is where that prim is stored in Fabric + fabric_index = mapping[view_index] + + # decompose transform matrix + position, rotation, scale = _decompose_transformation_matrix(wp.mat44f(fabric_matrices[fabric_index])) + # extract position - write to sequential output array (check if array has elements) + if array_positions.shape[0] > 0: + array_positions[output_index, 0] = position[0] + array_positions[output_index, 1] = position[1] + array_positions[output_index, 2] = position[2] + # extract orientation (Warp quaternion is xyzw, convert to wxyz) + if array_orientations.shape[0] > 0: + array_orientations[output_index, 0] = rotation[3] # w + array_orientations[output_index, 1] = rotation[0] # x + array_orientations[output_index, 2] = rotation[1] # y + array_orientations[output_index, 3] = rotation[2] # z + # extract scale + if array_scales.shape[0] > 0: + array_scales[output_index, 0] = scale[0] + array_scales[output_index, 1] = scale[1] + array_scales[output_index, 2] = scale[2] + + +@wp.kernel(enable_backward=False) +def compose_fabric_transformation_matrix_from_warp_arrays( + fabric_matrices: FabricArrayMat44d, + array_positions: ArrayFloat32_2d, + array_orientations: ArrayFloat32_2d, + array_scales: ArrayFloat32_2d, + broadcast_positions: bool, + broadcast_orientations: bool, + broadcast_scales: bool, + indices: ArrayUInt32, + mapping: ArrayUInt32, +): + """Compose Fabric transformation matrices from position, orientation, and scale arrays. + + This kernel updates Fabric's omni:fabric:worldMatrix attribute from separate component arrays. + It handles the quaternion convention conversion (Isaac Lab uses wxyz, Warp uses xyzw). + + After calling this kernel, IFabricHierarchy.updateWorldXforms() should be called to + propagate changes through the hierarchy. + + Args: + fabric_matrices: Fabric array containing 4x4 transformation matrices to update + array_positions: Input array for positions (N, 3) or None + array_orientations: Input array for quaternions in wxyz format (N, 4) or None + array_scales: Input array for scales (N, 3) or None + broadcast_positions: If True, use first position for all prims + broadcast_orientations: If True, use first orientation for all prims + broadcast_scales: If True, use first scale for all prims + indices: View indices to process + mapping: Mapping from view indices to fabric indices + """ + i = wp.tid() + # resolve fabric index + fabric_index = mapping[indices[i]] + # decompose current transform matrix to get existing values + position, rotation, scale = _decompose_transformation_matrix(wp.mat44f(fabric_matrices[fabric_index])) + # update position (check if array has elements, not just if it exists) + if array_positions.shape[0] > 0: + if broadcast_positions: + index = 0 + else: + index = i + position[0] = array_positions[index, 0] + position[1] = array_positions[index, 1] + position[2] = array_positions[index, 2] + # update orientation (convert from wxyz to xyzw for Warp) + if array_orientations.shape[0] > 0: + if broadcast_orientations: + index = 0 + else: + index = i + rotation[0] = array_orientations[index, 1] # x + rotation[1] = array_orientations[index, 2] # y + rotation[2] = array_orientations[index, 3] # z + rotation[3] = array_orientations[index, 0] # w + # update scale + if array_scales.shape[0] > 0: + if broadcast_scales: + index = 0 + else: + index = i + scale[0] = array_scales[index, 0] + scale[1] = array_scales[index, 1] + scale[2] = array_scales[index, 2] + # set transform matrix (need transpose for column-major ordering) + # Using transform_compose as wp.matrix() is deprecated + fabric_matrices[fabric_index] = wp.mat44d( # type: ignore[arg-type] + wp.transpose(wp.transform_compose(position, rotation, scale)) # type: ignore[arg-type] + ) + + +@wp.func +def _decompose_transformation_matrix(m: Any): # -> tuple[wp.vec3f, wp.quatf, wp.vec3f] + """Decompose a 4x4 transformation matrix into position, orientation, and scale. + + Args: + m: 4x4 transformation matrix + + Returns: + Tuple of (position, rotation_quaternion, scale) + """ + # extract position from translation column + position = wp.vec3f(m[3, 0], m[3, 1], m[3, 2]) + # extract rotation matrix components + r00, r01, r02 = m[0, 0], m[0, 1], m[0, 2] + r10, r11, r12 = m[1, 0], m[1, 1], m[1, 2] + r20, r21, r22 = m[2, 0], m[2, 1], m[2, 2] + # get scale magnitudes from column vectors + sx = wp.sqrt(r00 * r00 + r01 * r01 + r02 * r02) + sy = wp.sqrt(r10 * r10 + r11 * r11 + r12 * r12) + sz = wp.sqrt(r20 * r20 + r21 * r21 + r22 * r22) + # normalize rotation matrix components by scale + if sx != 0.0: + r00 /= sx + r01 /= sx + r02 /= sx + if sy != 0.0: + r10 /= sy + r11 /= sy + r12 /= sy + if sz != 0.0: + r20 /= sz + r21 /= sz + r22 /= sz + # extract rotation quaternion from normalized rotation matrix + rotation = wp.quat_from_matrix( # type: ignore[arg-type] + wp.transpose(wp.mat33f(r00, r01, r02, r10, r11, r12, r20, r21, r22)) # type: ignore[arg-type] + ) + # extract scale + scale = wp.vec3f(sx, sy, sz) + return position, rotation, scale diff --git a/source/isaaclab/isaaclab/utils/warp/kernels.py b/source/isaaclab/isaaclab/utils/warp/kernels.py index 2fe544651f50..cf56e34ed45a 100644 --- a/source/isaaclab/isaaclab/utils/warp/kernels.py +++ b/source/isaaclab/isaaclab/utils/warp/kernels.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +9,10 @@ import warp as wp +## +# Raycasting +## + @wp.kernel(enable_backward=False) def raycast_mesh_kernel( @@ -75,6 +79,171 @@ def raycast_mesh_kernel( ray_face_id[tid] = f +@wp.kernel(enable_backward=False) +def raycast_static_meshes_kernel( + mesh: wp.array2d(dtype=wp.uint64), + ray_starts: wp.array2d(dtype=wp.vec3), + ray_directions: wp.array2d(dtype=wp.vec3), + ray_hits: wp.array2d(dtype=wp.vec3), + ray_distance: wp.array2d(dtype=wp.float32), + ray_normal: wp.array2d(dtype=wp.vec3), + ray_face_id: wp.array2d(dtype=wp.int32), + ray_mesh_id: wp.array2d(dtype=wp.int16), + max_dist: float = 1e6, + return_normal: int = False, + return_face_id: int = False, + return_mesh_id: int = False, +): + """Performs ray-casting against multiple static meshes. + + This function performs ray-casting against the given meshes using the provided ray start positions + and directions. The resulting ray hit positions are stored in the :obj:`ray_hits` array. + + The function utilizes the ``mesh_query_ray`` method from the ``wp`` module to perform the actual ray-casting + operation. The maximum ray-cast distance is set to ``1e6`` units. + + .. note:: + That the ``ray_starts``, ``ray_directions``, and ``ray_hits`` arrays should have compatible shapes + and data types to ensure proper execution. Additionally, they all must be in the same frame. + + This kernel differs from the :meth:`raycast_dynamic_meshes_kernel` in that it does not take into + account the mesh's position and rotation. This kernel is useful for ray-casting against static meshes + that are not expected to move. + + Args: + mesh: The input mesh. The ray-casting is performed against this mesh on the device specified by the + `mesh`'s `device` attribute. + ray_starts: The input ray start positions. Shape is (B, N, 3). + ray_directions: The input ray directions. Shape is (B, N, 3). + ray_hits: The output ray hit positions. Shape is (B, N, 3). + ray_distance: The output ray hit distances. Shape is (B, N,), if ``return_distance`` is True. Otherwise, + this array is not used. + ray_normal: The output ray hit normals. Shape is (B, N, 3), if ``return_normal`` is True. Otherwise, + this array is not used. + ray_face_id: The output ray hit face ids. Shape is (B, N,), if ``return_face_id`` is True. Otherwise, + this array is not used. + ray_mesh_id: The output ray hit mesh ids. Shape is (B, N,), if ``return_mesh_id`` is True. Otherwise, + this array is not used. + max_dist: The maximum ray-cast distance. Defaults to 1e6. + return_normal: Whether to return the ray hit normals. Defaults to False`. + return_face_id: Whether to return the ray hit face ids. Defaults to False. + return_mesh_id: Whether to return the mesh id. Defaults to False. + """ + # get the thread id + tid_mesh_id, tid_env, tid_ray = wp.tid() + + direction = ray_directions[tid_env, tid_ray] + start_pos = ray_starts[tid_env, tid_ray] + + # ray cast against the mesh and store the hit position + mesh_query_ray_t = wp.mesh_query_ray(mesh[tid_env, tid_mesh_id], start_pos, direction, max_dist) + + # if the ray hit, store the hit data + if mesh_query_ray_t.result: + wp.atomic_min(ray_distance, tid_env, tid_ray, mesh_query_ray_t.t) + # check if hit distance is less than the current hit distance, only then update the memory + # TODO, in theory we could use the output of atomic_min to avoid the non-thread safe next comparison + # however, warp atomic_min is returning the wrong values on gpu currently. + # FIXME https://github.com/NVIDIA/warp/issues/1058 + if mesh_query_ray_t.t == ray_distance[tid_env, tid_ray]: + # convert back to world space and update the hit data + ray_hits[tid_env, tid_ray] = start_pos + mesh_query_ray_t.t * direction + + # update the normal and face id if requested + if return_normal == 1: + ray_normal[tid_env, tid_ray] = mesh_query_ray_t.normal + if return_face_id == 1: + ray_face_id[tid_env, tid_ray] = mesh_query_ray_t.face + if return_mesh_id == 1: + ray_mesh_id[tid_env, tid_ray] = wp.int16(tid_mesh_id) + + +@wp.kernel(enable_backward=False) +def raycast_dynamic_meshes_kernel( + mesh: wp.array2d(dtype=wp.uint64), + ray_starts: wp.array2d(dtype=wp.vec3), + ray_directions: wp.array2d(dtype=wp.vec3), + ray_hits: wp.array2d(dtype=wp.vec3), + ray_distance: wp.array2d(dtype=wp.float32), + ray_normal: wp.array2d(dtype=wp.vec3), + ray_face_id: wp.array2d(dtype=wp.int32), + ray_mesh_id: wp.array2d(dtype=wp.int16), + mesh_positions: wp.array2d(dtype=wp.vec3), + mesh_rotations: wp.array2d(dtype=wp.quat), + max_dist: float = 1e6, + return_normal: int = False, + return_face_id: int = False, + return_mesh_id: int = False, +): + """Performs ray-casting against multiple meshes. + + This function performs ray-casting against the given meshes using the provided ray start positions + and directions. The resulting ray hit positions are stored in the :obj:`ray_hits` array. + + The function utilizes the ``mesh_query_ray`` method from the ``wp`` module to perform the actual ray-casting + operation. The maximum ray-cast distance is set to ``1e6`` units. + + + Note: + That the ``ray_starts``, ``ray_directions``, and ``ray_hits`` arrays should have compatible shapes + and data types to ensure proper execution. Additionally, they all must be in the same frame. + + All arguments are expected to be batched with the first dimension (B, batch) being the number of envs + and the second dimension (N, num_rays) being the number of rays. For Meshes, W is the number of meshes. + + Args: + mesh: The input mesh. The ray-casting is performed against this mesh on the device specified by the + `mesh`'s `device` attribute. + ray_starts: The input ray start positions. Shape is (B, N, 3). + ray_directions: The input ray directions. Shape is (B, N, 3). + ray_hits: The output ray hit positions. Shape is (B, N, 3). + ray_distance: The output ray hit distances. Shape is (B, N,), if ``return_distance`` is True. Otherwise, + this array is not used. + ray_normal: The output ray hit normals. Shape is (B, N, 3), if ``return_normal`` is True. Otherwise, + this array is not used. + ray_face_id: The output ray hit face ids. Shape is (B, N,), if ``return_face_id`` is True. Otherwise, + this array is not used. + ray_mesh_id: The output ray hit mesh ids. Shape is (B, N,), if ``return_mesh_id`` is True. Otherwise, + this array is not used. + mesh_positions: The input mesh positions in world frame. Shape is (W, 3). + mesh_rotations: The input mesh rotations in world frame. Shape is (W, 4). + max_dist: The maximum ray-cast distance. Defaults to 1e6. + return_normal: Whether to return the ray hit normals. Defaults to False`. + return_face_id: Whether to return the ray hit face ids. Defaults to False. + return_mesh_id: Whether to return the mesh id. Defaults to False. + """ + # get the thread id + tid_mesh_id, tid_env, tid_ray = wp.tid() + + mesh_pose = wp.transform(mesh_positions[tid_env, tid_mesh_id], mesh_rotations[tid_env, tid_mesh_id]) + mesh_pose_inv = wp.transform_inverse(mesh_pose) + direction = wp.transform_vector(mesh_pose_inv, ray_directions[tid_env, tid_ray]) + start_pos = wp.transform_point(mesh_pose_inv, ray_starts[tid_env, tid_ray]) + + # ray cast against the mesh and store the hit position + mesh_query_ray_t = wp.mesh_query_ray(mesh[tid_env, tid_mesh_id], start_pos, direction, max_dist) + # if the ray hit, store the hit data + if mesh_query_ray_t.result: + wp.atomic_min(ray_distance, tid_env, tid_ray, mesh_query_ray_t.t) + # check if hit distance is less than the current hit distance, only then update the memory + # TODO, in theory we could use the output of atomic_min to avoid the non-thread safe next comparison + # however, warp atomic_min is returning the wrong values on gpu currently. + # FIXME https://github.com/NVIDIA/warp/issues/1058 + if mesh_query_ray_t.t == ray_distance[tid_env, tid_ray]: + # convert back to world space and update the hit data + hit_pos = start_pos + mesh_query_ray_t.t * direction + ray_hits[tid_env, tid_ray] = wp.transform_point(mesh_pose, hit_pos) + + # update the normal and face id if requested + if return_normal == 1: + n = wp.transform_vector(mesh_pose, mesh_query_ray_t.normal) + ray_normal[tid_env, tid_ray] = n + if return_face_id == 1: + ray_face_id[tid_env, tid_ray] = mesh_query_ray_t.face + if return_mesh_id == 1: + ray_mesh_id[tid_env, tid_ray] = wp.int16(tid_mesh_id) + + @wp.kernel(enable_backward=False) def reshape_tiled_image( tiled_image_buffer: Any, @@ -131,3 +300,170 @@ def reshape_tiled_image( reshape_tiled_image, {"tiled_image_buffer": wp.array(dtype=wp.float32), "batched_image": wp.array(dtype=wp.float32, ndim=4)}, ) + +## +# Wrench Composer +## + + +@wp.func +def cast_to_link_frame(position: wp.vec3f, link_position: wp.vec3f, is_global: bool) -> wp.vec3f: + """Casts a position to the link frame of the body. + + Args: + position: The position to cast. + link_position: The link frame position. + is_global: Whether the position is in the global frame. + + Returns: + The position in the link frame of the body. + """ + if is_global: + return position - link_position + else: + return position + + +@wp.func +def cast_force_to_link_frame(force: wp.vec3f, link_quat: wp.quatf, is_global: bool) -> wp.vec3f: + """Casts a force to the link frame of the body. + + Args: + force: The force to cast. + link_quat: The link frame quaternion. + is_global: Whether the force is applied in the global frame. + Returns: + The force in the link frame of the body. + """ + if is_global: + return wp.quat_rotate_inv(link_quat, force) + else: + return force + + +@wp.func +def cast_torque_to_link_frame(torque: wp.vec3f, link_quat: wp.quatf, is_global: bool) -> wp.vec3f: + """Casts a torque to the link frame of the body. + + Args: + torque: The torque to cast. + link_quat: The link frame quaternion. + is_global: Whether the torque is applied in the global frame. + + Returns: + The torque in the link frame of the body. + """ + if is_global: + return wp.quat_rotate_inv(link_quat, torque) + else: + return torque + + +@wp.kernel +def add_forces_and_torques_at_position( + env_ids: wp.array(dtype=wp.int32), + body_ids: wp.array(dtype=wp.int32), + forces: wp.array2d(dtype=wp.vec3f), + torques: wp.array2d(dtype=wp.vec3f), + positions: wp.array2d(dtype=wp.vec3f), + link_positions: wp.array2d(dtype=wp.vec3f), + link_quaternions: wp.array2d(dtype=wp.quatf), + composed_forces_b: wp.array2d(dtype=wp.vec3f), + composed_torques_b: wp.array2d(dtype=wp.vec3f), + is_global: bool, +): + """Adds forces and torques to the composed force and torque at the user-provided positions. + When is_global is False, the user-provided positions are offsetting the application of the force relatively to the + link frame of the body. When is_global is True, the user-provided positions are the global positions of the force + application. + + Args: + env_ids: The environment ids. + body_ids: The body ids. + forces: The forces. + torques: The torques. + positions: The positions. + link_positions: The link frame positions. + link_quaternions: The link frame quaternions. + composed_forces_b: The composed forces. + composed_torques_b: The composed torques. + is_global: Whether the forces and torques are applied in the global frame. + """ + # get the thread id + tid_env, tid_body = wp.tid() + + # add the forces to the composed force, if the positions are provided, also adds a torque to the composed torque. + if forces: + # add the forces to the composed force + composed_forces_b[env_ids[tid_env], body_ids[tid_body]] += cast_force_to_link_frame( + forces[tid_env, tid_body], link_quaternions[env_ids[tid_env], body_ids[tid_body]], is_global + ) + # if there is a position offset, add a torque to the composed torque. + if positions: + composed_torques_b[env_ids[tid_env], body_ids[tid_body]] += wp.skew( + cast_to_link_frame( + positions[tid_env, tid_body], link_positions[env_ids[tid_env], body_ids[tid_body]], is_global + ) + ) @ cast_force_to_link_frame( + forces[tid_env, tid_body], link_quaternions[env_ids[tid_env], body_ids[tid_body]], is_global + ) + if torques: + composed_torques_b[env_ids[tid_env], body_ids[tid_body]] += cast_torque_to_link_frame( + torques[tid_env, tid_body], link_quaternions[env_ids[tid_env], body_ids[tid_body]], is_global + ) + + +@wp.kernel +def set_forces_and_torques_at_position( + env_ids: wp.array(dtype=wp.int32), + body_ids: wp.array(dtype=wp.int32), + forces: wp.array2d(dtype=wp.vec3f), + torques: wp.array2d(dtype=wp.vec3f), + positions: wp.array2d(dtype=wp.vec3f), + link_positions: wp.array2d(dtype=wp.vec3f), + link_quaternions: wp.array2d(dtype=wp.quatf), + composed_forces_b: wp.array2d(dtype=wp.vec3f), + composed_torques_b: wp.array2d(dtype=wp.vec3f), + is_global: bool, +): + """Sets forces and torques to the composed force and torque at the user-provided positions. + When is_global is False, the user-provided positions are offsetting the application of the force relatively + to the link frame of the body. When is_global is True, the user-provided positions are the global positions + of the force application. + + Args: + env_ids: The environment ids. + body_ids: The body ids. + forces: The forces. + torques: The torques. + positions: The positions. + link_positions: The link frame positions. + link_quaternions: The link frame quaternions. + composed_forces_b: The composed forces. + composed_torques_b: The composed torques. + is_global: Whether the forces and torques are applied in the global frame. + """ + # get the thread id + tid_env, tid_body = wp.tid() + + # set the torques to the composed torque + if torques: + composed_torques_b[env_ids[tid_env], body_ids[tid_body]] = cast_torque_to_link_frame( + torques[tid_env, tid_body], link_quaternions[env_ids[tid_env], body_ids[tid_body]], is_global + ) + # set the forces to the composed force, if the positions are provided, adds a torque to the composed torque + # from the force at that position. + if forces: + # set the forces to the composed force + composed_forces_b[env_ids[tid_env], body_ids[tid_body]] = cast_force_to_link_frame( + forces[tid_env, tid_body], link_quaternions[env_ids[tid_env], body_ids[tid_body]], is_global + ) + # if there is a position offset, set the torque from the force at that position. + if positions: + composed_torques_b[env_ids[tid_env], body_ids[tid_body]] = wp.skew( + cast_to_link_frame( + positions[tid_env, tid_body], link_positions[env_ids[tid_env], body_ids[tid_body]], is_global + ) + ) @ cast_force_to_link_frame( + forces[tid_env, tid_body], link_quaternions[env_ids[tid_env], body_ids[tid_body]], is_global + ) diff --git a/source/isaaclab/isaaclab/utils/warp/ops.py b/source/isaaclab/isaaclab/utils/warp/ops.py index a2db46c4b526..f7cc8ac01def 100644 --- a/source/isaaclab/isaaclab/utils/warp/ops.py +++ b/source/isaaclab/isaaclab/utils/warp/ops.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 @@ import numpy as np import torch - import warp as wp # disable warp module initialization messages @@ -18,6 +17,8 @@ # initialize the warp module wp.init() +from isaaclab.utils.math import convert_quat + from . import kernels @@ -127,6 +128,257 @@ def raycast_mesh( return ray_hits.to(device).view(shape), ray_distance, ray_normal, ray_face_id +def raycast_single_mesh( + ray_starts: torch.Tensor, + ray_directions: torch.Tensor, + mesh_id: int, + max_dist: float = 1e6, + return_distance: bool = False, + return_normal: bool = False, + return_face_id: bool = False, +) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]: + """Performs ray-casting against a mesh. + + Note that the :attr:`ray_starts` and :attr:`ray_directions`, and :attr:`ray_hits` should have compatible shapes + and data types to ensure proper execution. Additionally, they all must be in the same frame. + + Args: + ray_starts: The starting position of the rays. Shape (B, N, 3). + ray_directions: The ray directions for each ray. Shape (B, N, 3). + mesh_id: The warp mesh id to ray-cast against. + max_dist: The maximum distance to ray-cast. Defaults to 1e6. + return_distance: Whether to return the distance of the ray until it hits the mesh. Defaults to False. + return_normal: Whether to return the normal of the mesh face the ray hits. Defaults to False. + return_face_id: Whether to return the face id of the mesh face the ray hits. Defaults to False. + + Returns: + The ray hit position. Shape (B, N, 3). + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit distance. Shape (B, N,). + Will only return if :attr:`return_distance` is True, else returns None. + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit normal. Shape (B, N, 3). + Will only return if :attr:`return_normal` is True else returns None. + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit face id. Shape (B, N,). + Will only return if :attr:`return_face_id` is True else returns None. + The returned tensor contains :obj:`int(-1)` for missed hits. + """ + # cast mesh id into array + mesh_ids = wp.array2d( + [[mesh_id] for _ in range(ray_starts.shape[0])], dtype=wp.uint64, device=wp.device_from_torch(ray_starts.device) + ) + ray_hits, ray_distance, ray_normal, ray_face_id, ray_mesh_id = raycast_dynamic_meshes( + ray_starts=ray_starts, + ray_directions=ray_directions, + mesh_ids_wp=mesh_ids, + max_dist=max_dist, + return_distance=return_distance, + return_normal=return_normal, + return_face_id=return_face_id, + ) + + return ray_hits, ray_distance, ray_normal, ray_face_id + + +def raycast_dynamic_meshes( + ray_starts: torch.Tensor, + ray_directions: torch.Tensor, + mesh_ids_wp: wp.Array, + mesh_positions_w: torch.Tensor | None = None, + mesh_orientations_w: torch.Tensor | None = None, + max_dist: float = 1e6, + return_distance: bool = False, + return_normal: bool = False, + return_face_id: bool = False, + return_mesh_id: bool = False, +) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]: + """Performs ray-casting against multiple, dynamic meshes. + + Note that the :attr:`ray_starts` and :attr:`ray_directions`, and :attr:`ray_hits` should have compatible shapes + and data types to ensure proper execution. Additionally, they all must be in the same frame. + + If mesh positions and rotations are provided, they need to have to have the same shape as the + number of meshes. + + Args: + ray_starts: The starting position of the rays. Shape (B, N, 3). + ray_directions: The ray directions for each ray. Shape (B, N, 3). + mesh_ids_wp: The warp mesh ids to ray-cast against. Length (B, M). + mesh_positions_w: The world positions of the meshes. Shape (B, M, 3). + mesh_orientations_w: The world orientation as quaternion (wxyz) format. Shape (B, M, 4). + max_dist: The maximum distance to ray-cast. Defaults to 1e6. + return_distance: Whether to return the distance of the ray until it hits the mesh. Defaults to False. + return_normal: Whether to return the normal of the mesh face the ray hits. Defaults to False. + return_face_id: Whether to return the face id of the mesh face the ray hits. Defaults to False. + return_mesh_id: Whether to return the mesh id of the mesh face the ray hits. Defaults to False. + NOTE: the type of the returned tensor is torch.int16, so you can't have more than 32767 meshes. + + Returns: + The ray hit position. Shape (B, N, 3). + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit distance. Shape (B, N,). + Will only return if :attr:`return_distance` is True, else returns None. + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit normal. Shape (B, N, 3). + Will only return if :attr:`return_normal` is True else returns None. + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit face id. Shape (B, N,). + Will only return if :attr:`return_face_id` is True else returns None. + The returned tensor contains :obj:`int(-1)` for missed hits. + The ray hit mesh id. Shape (B, N,). + Will only return if :attr:`return_mesh_id` is True else returns None. + The returned tensor contains :obj:`-1` for missed hits. + """ + # extract device and shape information + shape = ray_starts.shape + device = ray_starts.device + + # device of the mesh + torch_device = wp.device_to_torch(mesh_ids_wp.device) + n_meshes = mesh_ids_wp.shape[1] + + n_envs = ray_starts.shape[0] + n_rays_per_env = ray_starts.shape[1] + + # reshape the tensors + ray_starts = ray_starts.to(torch_device).view(n_envs, n_rays_per_env, 3).contiguous() + ray_directions = ray_directions.to(torch_device).view(n_envs, n_rays_per_env, 3).contiguous() + + # create output tensor for the ray hits + ray_hits = torch.full((n_envs, n_rays_per_env, 3), float("inf"), device=torch_device).contiguous() + + # map the memory to warp arrays + ray_starts_wp = wp.from_torch(ray_starts, dtype=wp.vec3) + ray_directions_wp = wp.from_torch(ray_directions, dtype=wp.vec3) + ray_hits_wp = wp.from_torch(ray_hits, dtype=wp.vec3) + # required to check if a closer hit is reported, returned only if return_distance is true + ray_distance = torch.full( + ( + n_envs, + n_rays_per_env, + ), + float("inf"), + device=torch_device, + ).contiguous() + ray_distance_wp = wp.from_torch(ray_distance, dtype=wp.float32) + + if return_normal: + ray_normal = torch.full((n_envs, n_rays_per_env, 3), float("inf"), device=torch_device).contiguous() + ray_normal_wp = wp.from_torch(ray_normal, dtype=wp.vec3) + else: + ray_normal = None + ray_normal_wp = wp.empty((1, 1), dtype=wp.vec3, device=torch_device) + + if return_face_id: + ray_face_id = torch.ones( + ( + n_envs, + n_rays_per_env, + ), + dtype=torch.int32, + device=torch_device, + ).contiguous() * (-1) + ray_face_id_wp = wp.from_torch(ray_face_id, dtype=wp.int32) + else: + ray_face_id = None + ray_face_id_wp = wp.empty((1, 1), dtype=wp.int32, device=torch_device) + + if return_mesh_id: + ray_mesh_id = -torch.ones((n_envs, n_rays_per_env), dtype=torch.int16, device=torch_device).contiguous() + ray_mesh_id_wp = wp.from_torch(ray_mesh_id, dtype=wp.int16) + else: + ray_mesh_id = None + ray_mesh_id_wp = wp.empty((1, 1), dtype=wp.int16, device=torch_device) + + ## + # Call the warp kernels + ### + if mesh_positions_w is None and mesh_orientations_w is None: + # Static mesh case, no need to pass in positions and rotations. + # launch the warp kernel + wp.launch( + kernel=kernels.raycast_static_meshes_kernel, + dim=[n_meshes, n_envs, n_rays_per_env], + inputs=[ + mesh_ids_wp, + ray_starts_wp, + ray_directions_wp, + ray_hits_wp, + ray_distance_wp, + ray_normal_wp, + ray_face_id_wp, + ray_mesh_id_wp, + float(max_dist), + int(return_normal), + int(return_face_id), + int(return_mesh_id), + ], + device=torch_device, + ) + else: + # dynamic mesh case + if mesh_positions_w is None: + mesh_positions_wp_w = wp.zeros((n_envs, n_meshes), dtype=wp.vec3, device=torch_device) + else: + mesh_positions_w = mesh_positions_w.to(torch_device).view(n_envs, n_meshes, 3).contiguous() + mesh_positions_wp_w = wp.from_torch(mesh_positions_w, dtype=wp.vec3) + + if mesh_orientations_w is None: + # Note (zrene): This is a little bit ugly, since it requires to initialize torch memory first + # But I couldn't find a better way to initialize a quaternion identity in warp + # wp.zeros(1, dtype=wp.quat, device=torch_device) gives all zero quaternion + quat_identity = torch.tensor([0, 0, 0, 1], dtype=torch.float32, device=torch_device).repeat( + n_envs, n_meshes, 1 + ) + mesh_quat_wp_w = wp.from_torch(quat_identity, dtype=wp.quat) + else: + mesh_orientations_w = convert_quat( + mesh_orientations_w.to(dtype=torch.float32, device=torch_device), "xyzw" + ).contiguous() + mesh_quat_wp_w = wp.from_torch(mesh_orientations_w, dtype=wp.quat) + + # launch the warp kernel + wp.launch( + kernel=kernels.raycast_dynamic_meshes_kernel, + dim=[n_meshes, n_envs, n_rays_per_env], + inputs=[ + mesh_ids_wp, + ray_starts_wp, + ray_directions_wp, + ray_hits_wp, + ray_distance_wp, + ray_normal_wp, + ray_face_id_wp, + ray_mesh_id_wp, + mesh_positions_wp_w, + mesh_quat_wp_w, + float(max_dist), + int(return_normal), + int(return_face_id), + int(return_mesh_id), + ], + device=torch_device, + ) + ## + # Cleanup and convert back to torch tensors + ## + + # NOTE: Synchronize is not needed anymore, but we keep it for now. Check with @dhoeller. + wp.synchronize() + + if return_distance: + ray_distance = ray_distance.to(device).view(shape[:2]) + if return_normal: + ray_normal = ray_normal.to(device).view(shape) + if return_face_id: + ray_face_id = ray_face_id.to(device).view(shape[:2]) + if return_mesh_id: + ray_mesh_id = ray_mesh_id.to(device).view(shape[:2]) + + return ray_hits.to(device).view(shape), ray_distance, ray_normal, ray_face_id, ray_mesh_id + + def convert_to_warp_mesh(points: np.ndarray, indices: np.ndarray, device: str) -> wp.Mesh: """Create a warp mesh object with a mesh defined from vertices and triangles. diff --git a/source/isaaclab/isaaclab/utils/wrench_composer.py b/source/isaaclab/isaaclab/utils/wrench_composer.py new file mode 100644 index 000000000000..8bd42f81e9e6 --- /dev/null +++ b/source/isaaclab/isaaclab/utils/wrench_composer.py @@ -0,0 +1,349 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +import warp as wp + +from isaaclab.utils.math import convert_quat +from isaaclab.utils.warp.kernels import add_forces_and_torques_at_position, set_forces_and_torques_at_position + +if TYPE_CHECKING: + from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection + + +class WrenchComposer: + def __init__(self, asset: Articulation | RigidObject | RigidObjectCollection) -> None: + """Wrench composer. + + This class is used to compose forces and torques at the body's link frame. + It can compose global wrenches and local wrenches. The result is always in the link frame of the body. + + Args: + asset: Asset to use. Defaults to None. + """ + self.num_envs = asset.num_instances + # Avoid isinstance to prevent circular import issues, use attribute presence instead. + if hasattr(asset, "num_bodies"): + self.num_bodies = asset.num_bodies + else: + self.num_bodies = asset.num_objects + self.device = asset.device + self._asset = asset + self._active = False + + # Avoid isinstance here due to potential circular import issues; check by attribute presence instead. + if hasattr(self._asset.data, "body_link_pos_w") and hasattr(self._asset.data, "body_link_quat_w"): + self._get_link_position_fn = lambda a=self._asset: a.data.body_link_pos_w[..., :3] + self._get_link_quaternion_fn = lambda a=self._asset: a.data.body_link_quat_w[..., :4] + elif hasattr(self._asset.data, "object_link_pos_w") and hasattr(self._asset.data, "object_link_quat_w"): + self._get_link_position_fn = lambda a=self._asset: a.data.object_link_pos_w[..., :3] + self._get_link_quaternion_fn = lambda a=self._asset: a.data.object_link_quat_w[..., :4] + else: + raise ValueError(f"Unsupported asset type: {self._asset.__class__.__name__}") + + # Create buffers + self._composed_force_b = wp.zeros((self.num_envs, self.num_bodies), dtype=wp.vec3f, device=self.device) + self._composed_torque_b = wp.zeros((self.num_envs, self.num_bodies), dtype=wp.vec3f, device=self.device) + self._ALL_ENV_INDICES_WP = wp.from_torch( + torch.arange(self.num_envs, dtype=torch.int32, device=self.device), dtype=wp.int32 + ) + self._ALL_BODY_INDICES_WP = wp.from_torch( + torch.arange(self.num_bodies, dtype=torch.int32, device=self.device), dtype=wp.int32 + ) + + # Pinning the composed force and torque to the torch tensor to avoid copying the data to the torch tensor + self._composed_force_b_torch = wp.to_torch(self._composed_force_b) + self._composed_torque_b_torch = wp.to_torch(self._composed_torque_b) + # Pinning the environment and body indices to the torch tensor to allow for slicing. + self._ALL_ENV_INDICES_TORCH = wp.to_torch(self._ALL_ENV_INDICES_WP) + self._ALL_BODY_INDICES_TORCH = wp.to_torch(self._ALL_BODY_INDICES_WP) + + # Flag to check if the link poses have been updated. + self._link_poses_updated = False + + @property + def active(self) -> bool: + """Whether the wrench composer is active.""" + return self._active + + @property + def composed_force(self) -> wp.array: + """Composed force at the body's link frame. + + .. note:: If some of the forces are applied in the global frame, the composed force will be in the link frame + of the body. + + Returns: + wp.array: Composed force at the body's link frame. (num_envs, num_bodies, 3) + """ + return self._composed_force_b + + @property + def composed_torque(self) -> wp.array: + """Composed torque at the body's link frame. + + .. note:: If some of the torques are applied in the global frame, the composed torque will be in the link frame + of the body. + + Returns: + wp.array: Composed torque at the body's link frame. (num_envs, num_bodies, 3) + """ + return self._composed_torque_b + + @property + def composed_force_as_torch(self) -> torch.Tensor: + """Composed force at the body's link frame as torch tensor. + + .. note:: If some of the forces are applied in the global frame, the composed force will be in the link frame + of the body. + + Returns: + torch.Tensor: Composed force at the body's link frame. (num_envs, num_bodies, 3) + """ + return self._composed_force_b_torch + + @property + def composed_torque_as_torch(self) -> torch.Tensor: + """Composed torque at the body's link frame as torch tensor. + + .. note:: If some of the torques are applied in the global frame, the composed torque will be in the link frame + of the body. + + Returns: + torch.Tensor: Composed torque at the body's link frame. (num_envs, num_bodies, 3) + """ + return self._composed_torque_b_torch + + def add_forces_and_torques( + self, + forces: wp.array | torch.Tensor | None = None, + torques: wp.array | torch.Tensor | None = None, + positions: wp.array | torch.Tensor | None = None, + body_ids: wp.array | torch.Tensor | None = None, + env_ids: wp.array | torch.Tensor | None = None, + is_global: bool = False, + ): + """Add forces and torques to the composed force and torque. + + Composed force and torque are the sum of all the forces and torques applied to the body. + It can compose global wrenches and local wrenches. The result is always in the link frame of the body. + + The user can provide any combination of forces, torques, and positions. + + .. note:: Users may want to call `reset` function after every simulation step to ensure no force is carried + over to the next step. However, this may not necessary if the user calls `set_forces_and_torques` function + instead of `add_forces_and_torques`. + + Args: + forces: Forces. (num_envs, num_bodies, 3). Defaults to None. + torques: Torques. (num_envs, num_bodies, 3). Defaults to None. + positions: Positions. (num_envs, num_bodies, 3). Defaults to None. + body_ids: Body ids. (num_envs, num_bodies). Defaults to None (all bodies). + env_ids: Environment ids. (num_envs). Defaults to None (all environments). + is_global: Whether the forces and torques are applied in the global frame. Defaults to False. + + Raises: + ValueError: If the type of the input is not supported. + ValueError: If the input is a slice and it is not None. + """ + # Resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES_WP + elif isinstance(env_ids, torch.Tensor): + env_ids = wp.from_torch(env_ids.to(torch.int32), dtype=wp.int32) + elif isinstance(env_ids, list): + env_ids = wp.array(env_ids, dtype=wp.int32, device=self.device) + elif isinstance(env_ids, slice): + if env_ids == slice(None): + env_ids = self._ALL_ENV_INDICES_WP + else: + raise ValueError(f"Doesn't support slice input for env_ids: {env_ids}") + # -- body_ids + if body_ids is None: + body_ids = self._ALL_BODY_INDICES_WP + elif isinstance(body_ids, torch.Tensor): + body_ids = wp.from_torch(body_ids.to(torch.int32), dtype=wp.int32) + elif isinstance(body_ids, list): + body_ids = wp.array(body_ids, dtype=wp.int32, device=self.device) + elif isinstance(body_ids, slice): + if body_ids == slice(None): + body_ids = self._ALL_BODY_INDICES_WP + else: + raise ValueError(f"Doesn't support slice input for body_ids: {body_ids}") + + # Resolve remaining inputs + # -- don't launch if no forces or torques are provided + if forces is None and torques is None: + return + if isinstance(forces, torch.Tensor): + forces = wp.from_torch(forces, dtype=wp.vec3f) + if isinstance(torques, torch.Tensor): + torques = wp.from_torch(torques, dtype=wp.vec3f) + if isinstance(positions, torch.Tensor): + positions = wp.from_torch(positions, dtype=wp.vec3f) + + # Get the link positions and quaternions + if not self._link_poses_updated: + self._link_positions = wp.from_torch(self._get_link_position_fn().clone(), dtype=wp.vec3f) + self._link_quaternions = wp.from_torch( + convert_quat(self._get_link_quaternion_fn().clone(), to="xyzw"), dtype=wp.quatf + ) + self._link_poses_updated = True + + # Set the active flag to true + self._active = True + + wp.launch( + add_forces_and_torques_at_position, + dim=(env_ids.shape[0], body_ids.shape[0]), + inputs=[ + env_ids, + body_ids, + forces, + torques, + positions, + self._link_positions, + self._link_quaternions, + self._composed_force_b, + self._composed_torque_b, + is_global, + ], + device=self.device, + ) + + def set_forces_and_torques( + self, + forces: wp.array | torch.Tensor | None = None, + torques: wp.array | torch.Tensor | None = None, + positions: wp.array | torch.Tensor | None = None, + body_ids: wp.array | torch.Tensor | None = None, + env_ids: wp.array | torch.Tensor | None = None, + is_global: bool = False, + ): + """Set forces and torques to the composed force and torque. + + Composed force and torque are the sum of all the forces and torques applied to the body. + It can compose global wrenches and local wrenches. The result is always in the link frame of the body. + + The user can provide any combination of forces, torques, and positions. + + Args: + forces: Forces. (num_envs, num_bodies, 3). Defaults to None. + torques: Torques. (num_envs, num_bodies, 3). Defaults to None. + positions: Positions. (num_envs, num_bodies, 3). Defaults to None. + body_ids: Body ids. (num_envs, num_bodies). Defaults to None (all bodies). + env_ids: Environment ids. (num_envs). Defaults to None (all environments). + is_global: Whether the forces and torques are applied in the global frame. Defaults to False. + + Raises: + ValueError: If the type of the input is not supported. + ValueError: If the input is a slice and it is not None. + """ + # Resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES_WP + elif isinstance(env_ids, torch.Tensor): + env_ids = wp.from_torch(env_ids.to(torch.int32), dtype=wp.int32) + elif isinstance(env_ids, list): + env_ids = wp.array(env_ids, dtype=wp.int32, device=self.device) + elif isinstance(env_ids, slice): + if env_ids == slice(None): + env_ids = self._ALL_ENV_INDICES_WP + else: + raise ValueError(f"Doesn't support slice input for env_ids: {env_ids}") + # -- body_ids + if body_ids is None: + body_ids = self._ALL_BODY_INDICES_WP + elif isinstance(body_ids, torch.Tensor): + body_ids = wp.from_torch(body_ids.to(torch.int32), dtype=wp.int32) + elif isinstance(body_ids, list): + body_ids = wp.array(body_ids, dtype=wp.int32, device=self.device) + elif isinstance(body_ids, slice): + if body_ids == slice(None): + body_ids = self._ALL_BODY_INDICES_WP + else: + raise ValueError(f"Doesn't support slice input for body_ids: {body_ids}") + # Resolve remaining inputs + # -- don't launch if no forces or torques are provided + if forces is None and torques is None: + return + if forces is None: + forces = wp.empty((0, 0), dtype=wp.vec3f, device=self.device) + elif isinstance(forces, torch.Tensor): + forces = wp.from_torch(forces, dtype=wp.vec3f) + if torques is None: + torques = wp.empty((0, 0), dtype=wp.vec3f, device=self.device) + elif isinstance(torques, torch.Tensor): + torques = wp.from_torch(torques, dtype=wp.vec3f) + if positions is None: + positions = wp.empty((0, 0), dtype=wp.vec3f, device=self.device) + elif isinstance(positions, torch.Tensor): + positions = wp.from_torch(positions, dtype=wp.vec3f) + + # Get the link positions and quaternions + if not self._link_poses_updated: + self._link_positions = wp.from_torch(self._get_link_position_fn().clone(), dtype=wp.vec3f) + self._link_quaternions = wp.from_torch( + convert_quat(self._get_link_quaternion_fn().clone(), to="xyzw"), dtype=wp.quatf + ) + self._link_poses_updated = True + + # Set the active flag to true + self._active = True + + wp.launch( + set_forces_and_torques_at_position, + dim=(env_ids.shape[0], body_ids.shape[0]), + inputs=[ + env_ids, + body_ids, + forces, + torques, + positions, + self._link_positions, + self._link_quaternions, + self._composed_force_b, + self._composed_torque_b, + is_global, + ], + device=self.device, + ) + + def reset(self, env_ids: wp.array | torch.Tensor | None = None): + """Reset the composed force and torque. + + This function will reset the composed force and torque to zero. + It will also make sure the link positions and quaternions are updated in the next call of the + `add_forces_and_torques` or `set_forces_and_torques` functions. + + .. note:: This function should be called after every simulation step / reset to ensure no force is carried + over to the next step. + """ + if env_ids is None: + self._composed_force_b.zero_() + self._composed_torque_b.zero_() + self._active = False + else: + indices = env_ids + if isinstance(env_ids, torch.Tensor): + indices = wp.from_torch(env_ids.to(torch.int32), dtype=wp.int32) + elif isinstance(env_ids, list): + indices = wp.array(env_ids, dtype=wp.int32, device=self.device) + elif isinstance(env_ids, slice): + if env_ids == slice(None): + indices = self._ALL_ENV_INDICES_WP + else: + indices = env_ids + + self._composed_force_b[indices].zero_() + self._composed_torque_b[indices].zero_() + + self._link_poses_updated = False diff --git a/source/isaaclab/setup.py b/source/isaaclab/setup.py index f0d07f6ecaf7..939ee294d8d4 100644 --- a/source/isaaclab/setup.py +++ b/source/isaaclab/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,8 +6,8 @@ """Installation script for the 'isaaclab' python package.""" import os -import toml +import toml from setuptools import setup # Obtain the extension data from the extension.toml file @@ -31,19 +31,20 @@ "trimesh", "pyglet<2", # image processing - "transformers", + "transformers==4.57.6", "einops", # needed for transformers, doesn't always auto-install "warp-lang", # make sure this is consistent with isaac sim version "pillow==11.3.0", # livestream - "starlette==0.45.3", + "starlette==0.49.1", # testing "pytest", "pytest-mock", "junitparser", "flatdict==4.0.1", "flaky", + "packaging", ] # Append Linux x86_64 and ARM64 deps via PEP 508 markers @@ -52,6 +53,7 @@ INSTALL_REQUIRES += [ # required by isaaclab.isaaclab.controllers.pink_ik f"pin-pink==3.1.0 ; platform_system == 'Linux' and ({SUPPORTED_ARCHS_ARM})", + f"daqp==0.7.2 ; platform_system == 'Linux' and ({SUPPORTED_ARCHS_ARM})", # required by isaaclab.devices.openxr.retargeters.humanoid.fourier.gr1_t2_dex_retargeting_utils f"dex-retargeting==0.4.6 ; platform_system == 'Linux' and ({SUPPORTED_ARCHS})", ] diff --git a/source/isaaclab/test/actuators/test_dc_motor.py b/source/isaaclab/test/actuators/test_dc_motor.py index 5c5f55b20042..26ad2de0526d 100644 --- a/source/isaaclab/test/actuators/test_dc_motor.py +++ b/source/isaaclab/test/actuators/test_dc_motor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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,9 +12,8 @@ """Rest of imports follows""" -import torch - import pytest +import torch from isaaclab.actuators import DCMotorCfg diff --git a/source/isaaclab/test/actuators/test_ideal_pd_actuator.py b/source/isaaclab/test/actuators/test_ideal_pd_actuator.py index b1ca5a3f693c..d77e5e12c34a 100644 --- a/source/isaaclab/test/actuators/test_ideal_pd_actuator.py +++ b/source/isaaclab/test/actuators/test_ideal_pd_actuator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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,9 +12,8 @@ """Rest of imports follows""" -import torch - import pytest +import torch from isaaclab.actuators import IdealPDActuatorCfg from isaaclab.utils.types import ArticulationActions @@ -68,9 +67,7 @@ def test_ideal_pd_actuator_init_minimum(num_envs, num_joints, device, usd_defaul torch.testing.assert_close(actuator.computed_effort, torch.zeros(num_envs, num_joints, device=device)) torch.testing.assert_close(actuator.applied_effort, torch.zeros(num_envs, num_joints, device=device)) - torch.testing.assert_close( - actuator.effort_limit, actuator._DEFAULT_MAX_EFFORT_SIM * torch.ones(num_envs, num_joints, device=device) - ) + torch.testing.assert_close(actuator.effort_limit, torch.inf * torch.ones(num_envs, num_joints, device=device)) torch.testing.assert_close( actuator.effort_limit_sim, actuator._DEFAULT_MAX_EFFORT_SIM * torch.ones(num_envs, num_joints, device=device) ) @@ -133,11 +130,11 @@ def test_ideal_pd_actuator_init_effort_limits(num_envs, num_joints, device, effo effort_lim_sim_expected = actuator._DEFAULT_MAX_EFFORT_SIM elif effort_lim is None and effort_lim_sim is not None: - effort_lim_expected = effort_lim_sim + effort_lim_expected = effort_lim_default effort_lim_sim_expected = effort_lim_sim elif effort_lim is None and effort_lim_sim is None: - effort_lim_expected = actuator._DEFAULT_MAX_EFFORT_SIM + effort_lim_expected = effort_lim_default effort_lim_sim_expected = actuator._DEFAULT_MAX_EFFORT_SIM elif effort_lim is not None and effort_lim_sim is not None: diff --git a/source/isaaclab/test/actuators/test_implicit_actuator.py b/source/isaaclab/test/actuators/test_implicit_actuator.py index ea854c5e0747..c4a26f2f9533 100644 --- a/source/isaaclab/test/actuators/test_implicit_actuator.py +++ b/source/isaaclab/test/actuators/test_implicit_actuator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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,9 +12,8 @@ """Rest of imports follows""" -import torch - import pytest +import torch from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.sim import build_simulation_context diff --git a/source/isaaclab/test/app/test_argparser_launch.py b/source/isaaclab/test/app/test_argparser_launch.py index 8347805fb607..683409dd190c 100644 --- a/source/isaaclab/test/app/test_argparser_launch.py +++ b/source/isaaclab/test/app/test_argparser_launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/app/test_env_var_launch.py b/source/isaaclab/test/app/test_env_var_launch.py index 8c52a5b7dac6..9ec07f932749 100644 --- a/source/isaaclab/test/app/test_env_var_launch.py +++ b/source/isaaclab/test/app/test_env_var_launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/app/test_kwarg_launch.py b/source/isaaclab/test/app/test_kwarg_launch.py index 8fb591e5b774..b2781637b722 100644 --- a/source/isaaclab/test/app/test_kwarg_launch.py +++ b/source/isaaclab/test/app/test_kwarg_launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/app/test_non_headless_launch.py b/source/isaaclab/test/app/test_non_headless_launch.py index 52c35a109167..eb8544b995cc 100644 --- a/source/isaaclab/test/app/test_non_headless_launch.py +++ b/source/isaaclab/test/app/test_non_headless_launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/assets/check_external_force.py b/source/isaaclab/test/assets/check_external_force.py index afa41df4a5e4..f038b907a1f0 100644 --- a/source/isaaclab/test/assets/check_external_force.py +++ b/source/isaaclab/test/assets/check_external_force.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -106,7 +106,7 @@ def main(): robot.write_joint_state_to_sim(joint_pos, joint_vel) robot.reset() # apply force - robot.set_external_force_and_torque( + robot.permanent_wrench_composer.set_forces_and_torques( external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids ) # reset command diff --git a/source/isaaclab/test/assets/check_fixed_base_assets.py b/source/isaaclab/test/assets/check_fixed_base_assets.py index 8a07be1c4133..c62c5a3334da 100644 --- a/source/isaaclab/test/assets/check_fixed_base_assets.py +++ b/source/isaaclab/test/assets/check_fixed_base_assets.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,8 +35,6 @@ import numpy as np import torch -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils from isaaclab.assets import Articulation @@ -75,12 +73,12 @@ def design_scene() -> tuple[dict, list[list[float]]]: origins = define_origins(num_origins=4, spacing=2.0) # Origin 1 with Franka Panda - prim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) + sim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0]) # -- Robot franka = Articulation(FRANKA_PANDA_CFG.replace(prim_path="/World/Origin1/Robot")) # Origin 2 with Anymal C - prim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) + sim_utils.create_prim("/World/Origin2", "Xform", translation=origins[1]) # -- Robot robot_cfg = ANYMAL_C_CFG.replace(prim_path="/World/Origin2/Robot") robot_cfg.spawn.articulation_props.fix_root_link = True diff --git a/source/isaaclab/test/assets/check_ridgeback_franka.py b/source/isaaclab/test/assets/check_ridgeback_franka.py index 543e9ae308f1..724cd8b2a080 100644 --- a/source/isaaclab/test/assets/check_ridgeback_franka.py +++ b/source/isaaclab/test/assets/check_ridgeback_franka.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/assets/test_articulation.py b/source/isaaclab/test/assets/test_articulation.py index dfacff5d2ec4..9a983ab34c1d 100644 --- a/source/isaaclab/test/assets/test_articulation.py +++ b/source/isaaclab/test/assets/test_articulation.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,11 +18,9 @@ """Rest everything follows.""" import ctypes -import torch -import isaacsim.core.utils.prims as prim_utils import pytest -from isaacsim.core.version import get_version +import torch import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils @@ -33,6 +31,7 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.sim import build_simulation_context from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.version import get_isaac_sim_version ## # Pre-defined configs @@ -175,7 +174,7 @@ def generate_articulation( # Create Top-level Xforms, one for each articulation for i in range(num_articulations): - prim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) + sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_.*/Robot")) return articulation, translations @@ -815,45 +814,37 @@ def test_external_force_buffer(sim, num_articulations, device): for step in range(5): # initiate force tensor external_wrench_b = torch.zeros(articulation.num_instances, len(body_ids), 6, device=sim.device) - external_wrench_positions_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device) if step == 0 or step == 3: # set a non-zero force force = 1 - position = 1 else: # set a zero force force = 0 - position = 0 # set force value external_wrench_b[:, :, 0] = force external_wrench_b[:, :, 3] = force - external_wrench_positions_b[:, :, 0] = position # apply force - if step == 0 or step == 3: - articulation.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], - body_ids=body_ids, - positions=external_wrench_positions_b, - is_global=True, - ) - else: - articulation.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], - body_ids=body_ids, - is_global=False, - ) + # TODO: Replace with wrench composer once the deprecation is complete + articulation.set_external_force_and_torque( + external_wrench_b[..., :3], + external_wrench_b[..., 3:], + body_ids=body_ids, + ) # check if the articulation's force and torque buffers are correctly updated for i in range(num_articulations): - assert articulation._external_force_b[i, 0, 0].item() == force - assert articulation._external_torque_b[i, 0, 0].item() == force - assert articulation._external_wrench_positions_b[i, 0, 0].item() == position - assert articulation._use_global_wrench_frame == (step == 0 or step == 3) + assert articulation.permanent_wrench_composer.composed_force_as_torch[i, 0, 0].item() == force + assert articulation.permanent_wrench_composer.composed_torque_as_torch[i, 0, 0].item() == force + + # Check if the instantaneous wrench is correctly added to the permanent wrench + articulation.instantaneous_wrench_composer.add_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + body_ids=body_ids, + ) # apply action to the articulation articulation.set_joint_position_target(articulation.data.default_joint_pos.clone()) @@ -908,6 +899,7 @@ def test_external_force_on_single_body(sim, num_articulations, device): # reset articulation articulation.reset() # apply force + # TODO: Replace with wrench composer once the deprecation is complete articulation.set_external_force_and_torque( external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids ) @@ -932,9 +924,11 @@ def test_external_force_on_single_body_at_position(sim, num_articulations, devic """Test application of external force on the base of the articulation at a given position. This test verifies that: - 1. External forces can be applied to specific bodies - 2. The forces affect the articulation's motion correctly - 3. The articulation responds to the forces as expected + 1. External forces can be applied to specific bodies at a given position + 2. External forces can be applied to specific bodies in the global frame + 3. External forces are calculated and composed correctly + 4. The forces affect the articulation's motion correctly + 5. The articulation responds to the forces as expected Args: sim: The simulation fixture @@ -949,14 +943,17 @@ def test_external_force_on_single_body_at_position(sim, num_articulations, devic body_ids, _ = articulation.find_bodies("base") # Sample a large force external_wrench_b = torch.zeros(articulation.num_instances, len(body_ids), 6, device=sim.device) - external_wrench_b[..., 2] = 1000.0 + external_wrench_b[..., 2] = 500.0 external_wrench_positions_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device) - external_wrench_positions_b[..., 0] = 0.0 external_wrench_positions_b[..., 1] = 1.0 - external_wrench_positions_b[..., 2] = 0.0 + + desired_force = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device) + desired_force[..., 2] = 1000.0 + desired_torque = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device) + desired_torque[..., 0] = 1000.0 # Now we are ready! - for _ in range(5): + for i in range(5): # reset root state root_state = articulation.data.default_root_state.clone() root_state[0, 0] = 2.5 # space them apart by 2.5m @@ -972,11 +969,33 @@ def test_external_force_on_single_body_at_position(sim, num_articulations, devic # reset articulation articulation.reset() # apply force - articulation.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], + is_global = False + + if i % 2 == 0: + body_com_pos_w = articulation.data.body_com_pos_w[:, body_ids, :3] + # is_global = True + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + external_wrench_positions_b += body_com_pos_w + else: + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + + articulation.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + positions=external_wrench_positions_b, body_ids=body_ids, + is_global=is_global, + ) + articulation.permanent_wrench_composer.add_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], positions=external_wrench_positions_b, + body_ids=body_ids, + is_global=is_global, ) # perform simulation for _ in range(100): @@ -1033,6 +1052,7 @@ def test_external_force_on_multiple_bodies(sim, num_articulations, device): # reset articulation articulation.reset() # apply force + # TODO: Replace with wrench composer once the deprecation is complete articulation.set_external_force_and_torque( external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids ) @@ -1058,9 +1078,11 @@ def test_external_force_on_multiple_bodies_at_position(sim, num_articulations, d """Test application of external force on the legs of the articulation at a given position. This test verifies that: - 1. External forces can be applied to multiple bodies - 2. The forces affect the articulation's motion correctly - 3. The articulation responds to the forces as expected + 1. External forces can be applied to multiple bodies at a given position + 2. External forces can be applied to multiple bodies in the global frame + 3. External forces are calculated and composed correctly + 4. The forces affect the articulation's motion correctly + 5. The articulation responds to the forces as expected Args: sim: The simulation fixture @@ -1076,14 +1098,17 @@ def test_external_force_on_multiple_bodies_at_position(sim, num_articulations, d body_ids, _ = articulation.find_bodies(".*_SHANK") # Sample a large force external_wrench_b = torch.zeros(articulation.num_instances, len(body_ids), 6, device=sim.device) - external_wrench_b[..., 2] = 1000.0 + external_wrench_b[..., 2] = 500.0 external_wrench_positions_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device) - external_wrench_positions_b[..., 0] = 0.0 external_wrench_positions_b[..., 1] = 1.0 - external_wrench_positions_b[..., 2] = 0.0 + + desired_force = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device) + desired_force[..., 2] = 1000.0 + desired_torque = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device) + desired_torque[..., 0] = 1000.0 # Now we are ready! - for _ in range(5): + for i in range(5): # reset root state articulation.write_root_pose_to_sim(articulation.data.default_root_state.clone()[:, :7]) articulation.write_root_velocity_to_sim(articulation.data.default_root_state.clone()[:, 7:]) @@ -1095,12 +1120,34 @@ def test_external_force_on_multiple_bodies_at_position(sim, num_articulations, d articulation.write_joint_state_to_sim(joint_pos, joint_vel) # reset articulation articulation.reset() + + is_global = False + if i % 2 == 0: + body_com_pos_w = articulation.data.body_com_pos_w[:, body_ids, :3] + is_global = True + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + external_wrench_positions_b += body_com_pos_w + else: + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + # apply force - articulation.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], + articulation.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + positions=external_wrench_positions_b, body_ids=body_ids, + is_global=is_global, + ) + articulation.permanent_wrench_composer.add_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], positions=external_wrench_positions_b, + body_ids=body_ids, + is_global=is_global, ) # perform simulation for _ in range(100): @@ -1372,10 +1419,16 @@ def test_setting_velocity_limit_explicit(sim, num_articulations, device, vel_lim @pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) @pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [1e2, None]) +@pytest.mark.parametrize("effort_limit", [1e2, 80.0, None]) @pytest.mark.isaacsim_ci def test_setting_effort_limit_implicit(sim, num_articulations, device, effort_limit_sim, effort_limit): - """Test setting of the effort limit for implicit actuators.""" + """Test setting of effort limit for implicit actuators. + + This test verifies the effort limit resolution logic for actuator models implemented in :class:`ActuatorBase`: + - Case 1: If USD value == actuator config value: values match correctly + - Case 2: If USD value != actuator config value: actuator config value is used + - Case 3: If actuator config value is None: USD value is used as default + """ articulation_cfg = generate_articulation_cfg( articulation_type="single_joint_implicit", effort_limit_sim=effort_limit_sim, @@ -1419,10 +1472,18 @@ def test_setting_effort_limit_implicit(sim, num_articulations, device, effort_li @pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) @pytest.mark.parametrize("effort_limit_sim", [1e5, None]) -@pytest.mark.parametrize("effort_limit", [1e2, None]) +@pytest.mark.parametrize("effort_limit", [80.0, 1e2, None]) @pytest.mark.isaacsim_ci def test_setting_effort_limit_explicit(sim, num_articulations, device, effort_limit_sim, effort_limit): - """Test setting of effort limit for explicit actuators.""" + """Test setting of effort limit for explicit actuators. + + This test verifies the effort limit resolution logic for actuator models implemented in :class:`ActuatorBase`: + - Case 1: If USD value == actuator config value: values match correctly + - Case 2: If USD value != actuator config value: actuator config value is used + - Case 3: If actuator config value is None: USD value is used as default + + """ + articulation_cfg = generate_articulation_cfg( articulation_type="single_joint_explicit", effort_limit_sim=effort_limit_sim, @@ -1436,6 +1497,9 @@ def test_setting_effort_limit_explicit(sim, num_articulations, device, effort_li # Play sim sim.reset() + # usd default effort limit is set to 80 + usd_default_effort_limit = 80.0 + # collect limit init values physx_effort_limit = articulation.root_physx_view.get_dof_max_forces().to(device) actuator_effort_limit = articulation.actuators["joint"].effort_limit @@ -1452,8 +1516,9 @@ def test_setting_effort_limit_explicit(sim, num_articulations, device, effort_li # check physx effort limit does not match the one explicit actuator has assert not (torch.allclose(actuator_effort_limit, physx_effort_limit)) else: - # check actuator effort_limit is the same as the PhysX default - torch.testing.assert_close(actuator_effort_limit, physx_effort_limit) + # When effort_limit is None, actuator should use USD default values + expected_actuator_effort_limit = torch.full_like(physx_effort_limit, usd_default_effort_limit) + torch.testing.assert_close(actuator_effort_limit, expected_actuator_effort_limit) # when using explicit actuators, the limits are set to high unless user overrides if effort_limit_sim is not None: @@ -1462,6 +1527,7 @@ def test_setting_effort_limit_explicit(sim, num_articulations, device, effort_li limit = ActuatorBase._DEFAULT_MAX_EFFORT_SIM # type: ignore # check physx internal value matches the expected sim value expected_effort_limit = torch.full_like(physx_effort_limit, limit) + torch.testing.assert_close(actuator_effort_limit_sim, expected_effort_limit) torch.testing.assert_close(physx_effort_limit, expected_effort_limit) @@ -1483,9 +1549,35 @@ def test_reset(sim, num_articulations, device): articulation.reset() # Reset should zero external forces and torques - assert not articulation.has_external_wrench - assert torch.count_nonzero(articulation._external_force_b) == 0 - assert torch.count_nonzero(articulation._external_torque_b) == 0 + assert not articulation._instantaneous_wrench_composer.active + assert not articulation._permanent_wrench_composer.active + assert torch.count_nonzero(articulation._instantaneous_wrench_composer.composed_force_as_torch) == 0 + assert torch.count_nonzero(articulation._instantaneous_wrench_composer.composed_torque_as_torch) == 0 + assert torch.count_nonzero(articulation._permanent_wrench_composer.composed_force_as_torch) == 0 + assert torch.count_nonzero(articulation._permanent_wrench_composer.composed_torque_as_torch) == 0 + + if num_articulations > 1: + num_bodies = articulation.num_bodies + # TODO: Replace with wrench composer once the deprecation is complete + articulation.set_external_force_and_torque( + forces=torch.ones((num_articulations, num_bodies, 3), device=device), + torques=torch.ones((num_articulations, num_bodies, 3), device=device), + ) + articulation.instantaneous_wrench_composer.add_forces_and_torques( + forces=torch.ones((num_articulations, num_bodies, 3), device=device), + torques=torch.ones((num_articulations, num_bodies, 3), device=device), + ) + articulation.reset(env_ids=torch.tensor([0], device=device)) + assert articulation._instantaneous_wrench_composer.active + assert articulation._permanent_wrench_composer.active + assert ( + torch.count_nonzero(articulation._instantaneous_wrench_composer.composed_force_as_torch) == num_bodies * 3 + ) + assert ( + torch.count_nonzero(articulation._instantaneous_wrench_composer.composed_torque_as_torch) == num_bodies * 3 + ) + assert torch.count_nonzero(articulation._permanent_wrench_composer.composed_force_as_torch) == num_bodies * 3 + assert torch.count_nonzero(articulation._permanent_wrench_composer.composed_torque_as_torch) == num_bodies * 3 @pytest.mark.parametrize("num_articulations", [1, 2]) @@ -1756,6 +1848,7 @@ def test_body_incoming_joint_wrench_b_single_joint(sim, num_articulations, devic articulation.set_joint_position_target(joint_pos) articulation.write_data_to_sim() for _ in range(50): + # TODO: Replace with wrench composer once the deprecation is complete articulation.set_external_force_and_torque(forces=external_force_vector_b, torques=external_torque_vector_b) articulation.write_data_to_sim() # perform step @@ -1938,7 +2031,7 @@ def test_spatial_tendons(sim, num_articulations, device): device: The device to run the simulation on """ # skip test if Isaac Sim version is less than 5.0 - if int(get_version()[2]) < 5: + if get_isaac_sim_version().major < 5: pytest.skip("Spatial tendons are not supported in Isaac Sim < 5.0. Please update to Isaac Sim 5.0 or later.") return articulation_cfg = generate_articulation_cfg(articulation_type="spatial_tendon_test_asset") @@ -2004,7 +2097,7 @@ def test_write_joint_frictions_to_sim(sim, num_articulations, device, add_ground # The static friction must be set first to be sure the dynamic friction is not greater than static # when both are set. articulation.write_joint_friction_coefficient_to_sim(friction) - if int(get_version()[2]) >= 5: + if get_isaac_sim_version().major >= 5: articulation.write_joint_dynamic_friction_coefficient_to_sim(dynamic_friction) articulation.write_joint_viscous_friction_coefficient_to_sim(viscous_friction) articulation.write_data_to_sim() @@ -2015,7 +2108,7 @@ def test_write_joint_frictions_to_sim(sim, num_articulations, device, add_ground # update buffers articulation.update(sim.cfg.dt) - if int(get_version()[2]) >= 5: + if get_isaac_sim_version().major >= 5: friction_props_from_sim = articulation.root_physx_view.get_dof_friction_properties() joint_friction_coeff_sim = friction_props_from_sim[:, :, 0] joint_dynamic_friction_coeff_sim = friction_props_from_sim[:, :, 1] @@ -2029,7 +2122,7 @@ def test_write_joint_frictions_to_sim(sim, num_articulations, device, add_ground # For Isaac Sim >= 5.0: also test the combined API that can set dynamic and viscous via # write_joint_friction_coefficient_to_sim; reset the sim to isolate this path. - if int(get_version()[2]) >= 5: + if get_isaac_sim_version().major >= 5: # Reset simulator to ensure a clean state for the alternative API path sim.reset() diff --git a/source/isaaclab/test/assets/test_deformable_object.py b/source/isaaclab/test/assets/test_deformable_object.py index 2d589573e695..4726a274462c 100644 --- a/source/isaaclab/test/assets/test_deformable_object.py +++ b/source/isaaclab/test/assets/test_deformable_object.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,13 +17,13 @@ """Rest everything follows.""" import ctypes -import torch -import carb -import isaacsim.core.utils.prims as prim_utils import pytest +import torch from flaky import flaky +import carb + import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils from isaaclab.assets import DeformableObject, DeformableObjectCfg @@ -58,7 +58,7 @@ def generate_cubes_scene( origins = torch.tensor([(i * 1.0, 0, height) for i in range(num_cubes)]).to(device) # Create Top-level Xforms, one for each cube for i, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) + sim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) # Resolve spawn configuration if has_api: diff --git a/source/isaaclab/test/assets/test_rigid_object.py b/source/isaaclab/test/assets/test_rigid_object.py index 6a0dc77b861a..8de5361e29ff 100644 --- a/source/isaaclab/test/assets/test_rigid_object.py +++ b/source/isaaclab/test/assets/test_rigid_object.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +17,10 @@ """Rest everything follows.""" import ctypes -import torch from typing import Literal -import isaacsim.core.utils.prims as prim_utils import pytest +import torch from flaky import flaky import isaaclab.sim as sim_utils @@ -63,7 +62,7 @@ def generate_cubes_scene( origins = torch.tensor([(i * 1.0, 0, height) for i in range(num_cubes)]).to(device) # Create Top-level Xforms, one for each cube for i, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) + sim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) # Resolve spawn configuration if api == "none": @@ -227,49 +226,38 @@ def test_external_force_buffer(device): # perform simulation for step in range(5): - # initiate force tensor external_wrench_b = torch.zeros(cube_object.num_instances, len(body_ids), 6, device=sim.device) - external_wrench_positions_b = torch.zeros(cube_object.num_instances, len(body_ids), 3, device=sim.device) if step == 0 or step == 3: # set a non-zero force force = 1 - position = 1 - is_global = True else: # set a zero force force = 0 - position = 0 - is_global = False # set force value external_wrench_b[:, :, 0] = force external_wrench_b[:, :, 3] = force - external_wrench_positions_b[:, :, 0] = position # apply force - if step == 0 or step == 3: - cube_object.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], - body_ids=body_ids, - positions=external_wrench_positions_b, - is_global=is_global, - ) - else: - cube_object.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], - body_ids=body_ids, - is_global=is_global, - ) + cube_object.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + body_ids=body_ids, + ) # check if the cube's force and torque buffers are correctly updated - assert cube_object._external_force_b[0, 0, 0].item() == force - assert cube_object._external_torque_b[0, 0, 0].item() == force - assert cube_object._external_wrench_positions_b[0, 0, 0].item() == position - assert cube_object._use_global_wrench_frame == (step == 0 or step == 3) + for i in range(cube_object.num_instances): + assert cube_object._permanent_wrench_composer.composed_force_as_torch[i, 0, 0].item() == force + assert cube_object._permanent_wrench_composer.composed_torque_as_torch[i, 0, 0].item() == force + + # Check if the instantaneous wrench is correctly added to the permanent wrench + cube_object.permanent_wrench_composer.add_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + body_ids=body_ids, + ) # apply action to the object cube_object.write_data_to_sim() @@ -290,6 +278,8 @@ def test_external_force_on_single_body(num_cubes, device): In this test, we apply a force equal to the weight of an object on the base of one of the objects. We check that the object does not move. For the other object, we do not apply any force and check that it falls down. + + We validate that this works when we apply the force in the global frame and in the local frame. """ # Generate cubes scene with build_simulation_context(device=device, add_ground_plane=True, auto_add_lighting=True) as sim: @@ -308,7 +298,7 @@ def test_external_force_on_single_body(num_cubes, device): external_wrench_b[0::2, :, 2] = 9.81 * cube_object.root_physx_view.get_masses()[0] # Now we are ready! - for _ in range(5): + for i in range(5): # reset root state root_state = cube_object.data.default_root_state.clone() @@ -320,9 +310,20 @@ def test_external_force_on_single_body(num_cubes, device): # reset object cube_object.reset() + is_global = False + if i % 2 == 0: + is_global = True + positions = cube_object.data.body_com_pos_w[:, body_ids, :3] + else: + positions = None + # apply force - cube_object.set_external_force_and_torque( - external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids + cube_object.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + positions=positions, + body_ids=body_ids, + is_global=is_global, ) # perform simulation for _ in range(5): @@ -351,6 +352,8 @@ def test_external_force_on_single_body_at_position(num_cubes, device): In this test, we apply a force equal to the weight of an object on the base of one of the objects at 1m in the Y direction, we check that the object rotates around it's X axis. For the other object, we do not apply any force and check that it falls down. + + We validate that this works when we apply the force in the global frame and in the local frame. """ # Generate cubes scene with build_simulation_context(device=device, add_ground_plane=True, auto_add_lighting=True) as sim: @@ -367,11 +370,16 @@ def test_external_force_on_single_body_at_position(num_cubes, device): external_wrench_b = torch.zeros(cube_object.num_instances, len(body_ids), 6, device=sim.device) external_wrench_positions_b = torch.zeros(cube_object.num_instances, len(body_ids), 3, device=sim.device) # Every 2nd cube should have a force applied to it - external_wrench_b[0::2, :, 2] = 9.81 * cube_object.root_physx_view.get_masses()[0] + external_wrench_b[0::2, :, 2] = 500.0 external_wrench_positions_b[0::2, :, 1] = 1.0 + # Desired force and torque + desired_force = torch.zeros(cube_object.num_instances, len(body_ids), 3, device=sim.device) + desired_force[0::2, :, 2] = 1000.0 + desired_torque = torch.zeros(cube_object.num_instances, len(body_ids), 3, device=sim.device) + desired_torque[0::2, :, 0] = 1000.0 # Now we are ready! - for _ in range(5): + for i in range(5): # reset root state root_state = cube_object.data.default_root_state.clone() @@ -383,12 +391,45 @@ def test_external_force_on_single_body_at_position(num_cubes, device): # reset object cube_object.reset() + is_global = False + if i % 2 == 0: + is_global = True + body_com_pos_w = cube_object.data.body_com_pos_w[:, body_ids, :3] + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + external_wrench_positions_b += body_com_pos_w + else: + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + # apply force - cube_object.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], + cube_object.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + positions=external_wrench_positions_b, + body_ids=body_ids, + is_global=is_global, + ) + cube_object.permanent_wrench_composer.add_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], positions=external_wrench_positions_b, body_ids=body_ids, + is_global=is_global, + ) + torch.testing.assert_close( + cube_object._permanent_wrench_composer.composed_force_as_torch[:, 0, :], + desired_force[:, 0, :], + rtol=1e-6, + atol=1e-7, + ) + torch.testing.assert_close( + cube_object._permanent_wrench_composer.composed_torque_as_torch[:, 0, :], + desired_torque[:, 0, :], + rtol=1e-6, + atol=1e-7, ) # perform simulation for _ in range(5): @@ -508,9 +549,12 @@ def test_reset_rigid_object(num_cubes, device): cube_object.reset() # Reset should zero external forces and torques - assert not cube_object.has_external_wrench - assert torch.count_nonzero(cube_object._external_force_b) == 0 - assert torch.count_nonzero(cube_object._external_torque_b) == 0 + assert not cube_object._instantaneous_wrench_composer.active + assert not cube_object._permanent_wrench_composer.active + assert torch.count_nonzero(cube_object._instantaneous_wrench_composer.composed_force_as_torch) == 0 + assert torch.count_nonzero(cube_object._instantaneous_wrench_composer.composed_torque_as_torch) == 0 + assert torch.count_nonzero(cube_object._permanent_wrench_composer.composed_force_as_torch) == 0 + assert torch.count_nonzero(cube_object._permanent_wrench_composer.composed_torque_as_torch) == 0 @pytest.mark.parametrize("num_cubes", [1, 2]) @@ -671,6 +715,7 @@ def test_rigid_body_with_static_friction(num_cubes, device): else: external_wrench_b[..., 0] = static_friction_coefficient * cube_mass * gravity_magnitude * 1.01 + # TODO: Replace with wrench composer once the deprecation is complete cube_object.set_external_force_and_torque( external_wrench_b[..., :3], external_wrench_b[..., 3:], @@ -999,7 +1044,6 @@ def test_write_root_state(num_cubes, device, with_offset, state_location): env_idx = env_idx.to(device) for i in range(10): - # perform step sim.step() # update buffers diff --git a/source/isaaclab/test/assets/test_rigid_object_collection.py b/source/isaaclab/test/assets/test_rigid_object_collection.py index 876a2904bf12..8d919bf4d7ad 100644 --- a/source/isaaclab/test/assets/test_rigid_object_collection.py +++ b/source/isaaclab/test/assets/test_rigid_object_collection.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 @@ """Rest everything follows.""" import ctypes -import torch -import isaacsim.core.utils.prims as prim_utils import pytest +import torch import isaaclab.sim as sim_utils from isaaclab.assets import RigidObjectCfg, RigidObjectCollection, RigidObjectCollectionCfg @@ -63,7 +62,7 @@ def generate_cubes_scene( origins = torch.tensor([(i * 3.0, 0, height) for i in range(num_envs)]).to(device) # Create Top-level Xforms, one for each cube for i, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) + sim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) # Resolve spawn configuration if has_api: @@ -233,39 +232,34 @@ def test_external_force_buffer(sim, device): for step in range(5): # initiate force tensor external_wrench_b = torch.zeros(object_collection.num_instances, len(object_ids), 6, device=sim.device) - external_wrench_positions_b = torch.zeros( - object_collection.num_instances, len(object_ids), 3, device=sim.device - ) # decide if zero or non-zero force if step == 0 or step == 3: force = 1.0 - position = 1.0 - is_global = True else: force = 0.0 - position = 0.0 - is_global = False # apply force to the object external_wrench_b[:, :, 0] = force external_wrench_b[:, :, 3] = force - external_wrench_positions_b[:, :, 0] = position - object_collection.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], - object_ids=object_ids, - positions=external_wrench_positions_b, - is_global=is_global, + object_collection.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + body_ids=object_ids, + env_ids=None, ) # check if the object collection's force and torque buffers are correctly updated for i in range(num_envs): - assert object_collection._external_force_b[i, 0, 0].item() == force - assert object_collection._external_torque_b[i, 0, 0].item() == force - assert object_collection._external_wrench_positions_b[i, 0, 0].item() == position - assert object_collection._use_global_wrench_frame == (step == 0 or step == 3) + assert object_collection._permanent_wrench_composer.composed_force_as_torch[i, 0, 0].item() == force + assert object_collection._permanent_wrench_composer.composed_torque_as_torch[i, 0, 0].item() == force + + object_collection.instantaneous_wrench_composer.add_forces_and_torques( + body_ids=object_ids, + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + ) # apply action to the object collection object_collection.write_data_to_sim() @@ -289,7 +283,7 @@ def test_external_force_on_single_body(sim, num_envs, num_cubes, device): # Every 2nd cube should have a force applied to it external_wrench_b[:, 0::2, 2] = 9.81 * object_collection.data.default_mass[:, 0::2, 0] - for _ in range(5): + for i in range(5): # reset object state object_state = object_collection.data.default_object_state.clone() # need to shift the position of the cubes otherwise they will be on top of each other @@ -298,11 +292,22 @@ def test_external_force_on_single_body(sim, num_envs, num_cubes, device): # reset object object_collection.reset() + is_global = False + if i % 2 == 0: + positions = object_collection.data.object_link_pos_w[:, object_ids, :3] + is_global = True + else: + positions = None + # apply force - object_collection.set_external_force_and_torque( - external_wrench_b[..., :3], external_wrench_b[..., 3:], object_ids=object_ids + object_collection.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + positions=positions, + body_ids=object_ids, + env_ids=None, + is_global=is_global, ) - for _ in range(10): # write data to sim object_collection.write_data_to_sim() @@ -340,10 +345,11 @@ def test_external_force_on_single_body_at_position(sim, num_envs, num_cubes, dev external_wrench_b = torch.zeros(object_collection.num_instances, len(object_ids), 6, device=sim.device) external_wrench_positions_b = torch.zeros(object_collection.num_instances, len(object_ids), 3, device=sim.device) # Every 2nd cube should have a force applied to it - external_wrench_b[:, 0::2, 2] = 9.81 * object_collection.data.default_mass[:, 0::2, 0] + external_wrench_b[:, 0::2, 2] = 500.0 external_wrench_positions_b[:, 0::2, 1] = 1.0 - for _ in range(5): + # Desired force and torque + for i in range(5): # reset object state object_state = object_collection.data.default_object_state.clone() # need to shift the position of the cubes otherwise they will be on top of each other @@ -352,12 +358,34 @@ def test_external_force_on_single_body_at_position(sim, num_envs, num_cubes, dev # reset object object_collection.reset() + is_global = False + if i % 2 == 0: + body_com_pos_w = object_collection.data.object_link_pos_w[:, object_ids, :3] + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + external_wrench_positions_b += body_com_pos_w + is_global = True + else: + external_wrench_positions_b[..., 0] = 0.0 + external_wrench_positions_b[..., 1] = 1.0 + external_wrench_positions_b[..., 2] = 0.0 + # apply force - object_collection.set_external_force_and_torque( - external_wrench_b[..., :3], - external_wrench_b[..., 3:], + object_collection.permanent_wrench_composer.set_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], + positions=external_wrench_positions_b, + body_ids=object_ids, + env_ids=None, + is_global=is_global, + ) + object_collection.permanent_wrench_composer.add_forces_and_torques( + forces=external_wrench_b[..., :3], + torques=external_wrench_b[..., 3:], positions=external_wrench_positions_b, - object_ids=object_ids, + body_ids=object_ids, + is_global=is_global, ) for _ in range(10): @@ -513,7 +541,7 @@ def test_object_state_properties(sim, num_envs, num_cubes, device, with_offset, torch.testing.assert_close(object_state_w[..., 3:7], object_link_state_w[..., 3:7]) # lin_vel will not match - # center of mass vel will be constant (i.e. spining around com) + # center of mass vel will be constant (i.e. spinning around com) torch.testing.assert_close( torch.zeros_like(object_com_state_w[..., 7:10]), object_com_state_w[..., 7:10], @@ -614,9 +642,12 @@ def test_reset_object_collection(sim, num_envs, num_cubes, device): object_collection.reset() # Reset should zero external forces and torques - assert not object_collection.has_external_wrench - assert torch.count_nonzero(object_collection._external_force_b) == 0 - assert torch.count_nonzero(object_collection._external_torque_b) == 0 + assert not object_collection._instantaneous_wrench_composer.active + assert not object_collection._permanent_wrench_composer.active + assert torch.count_nonzero(object_collection._instantaneous_wrench_composer.composed_force_as_torch) == 0 + assert torch.count_nonzero(object_collection._instantaneous_wrench_composer.composed_torque_as_torch) == 0 + assert torch.count_nonzero(object_collection._permanent_wrench_composer.composed_force_as_torch) == 0 + assert torch.count_nonzero(object_collection._permanent_wrench_composer.composed_torque_as_torch) == 0 @pytest.mark.parametrize("num_envs", [1, 3]) diff --git a/source/isaaclab/test/assets/test_surface_gripper.py b/source/isaaclab/test/assets/test_surface_gripper.py index c2f81143f598..86f112fdf984 100644 --- a/source/isaaclab/test/assets/test_surface_gripper.py +++ b/source/isaaclab/test/assets/test_surface_gripper.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,11 +16,8 @@ """Rest everything follows.""" -import torch - -import isaacsim.core.utils.prims as prim_utils import pytest -from isaacsim.core.version import get_version +import torch import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg @@ -34,6 +31,7 @@ ) from isaaclab.sim import build_simulation_context from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +from isaaclab.utils.version import get_isaac_sim_version # from isaacsim.robot.surface_gripper import GripperView @@ -112,7 +110,7 @@ def generate_surface_gripper( # Create Top-level Xforms, one for each articulation for i in range(num_surface_grippers): - prim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) + sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_.*/Robot")) surface_gripper_cfg = surface_gripper_cfg.replace(prim_path="/World/Env_.*/Robot/Gripper/SurfaceGripper") surface_gripper = SurfaceGripper(surface_gripper_cfg) @@ -173,8 +171,7 @@ def test_initialization(sim, num_articulations, device, add_ground_plane) -> Non device: The device to run the test on. add_ground_plane: Whether to add a ground plane to the simulation. """ - isaac_sim_version = get_version() - if int(isaac_sim_version[2]) < 5: + if get_isaac_sim_version().major < 5: return surface_gripper_cfg, articulation_cfg = generate_surface_gripper_cfgs(kinematic_enabled=False) surface_gripper, articulation, _ = generate_surface_gripper( @@ -208,8 +205,7 @@ def test_initialization(sim, num_articulations, device, add_ground_plane) -> Non @pytest.mark.isaacsim_ci def test_raise_error_if_not_cpu(sim, device, add_ground_plane) -> None: """Test that the SurfaceGripper raises an error if the device is not CPU.""" - isaac_sim_version = get_version() - if int(isaac_sim_version[2]) < 5: + if get_isaac_sim_version().major < 5: return num_articulations = 1 surface_gripper_cfg, articulation_cfg = generate_surface_gripper_cfgs(kinematic_enabled=False) diff --git a/source/isaaclab/test/controllers/test_controller_utils.py b/source/isaaclab/test/controllers/test_controller_utils.py index 9646b0e93980..80a839a847f4 100644 --- a/source/isaaclab/test/controllers/test_controller_utils.py +++ b/source/isaaclab/test/controllers/test_controller_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,9 +16,9 @@ # Import the function to test import tempfile -import torch import pytest +import torch from isaaclab.controllers.utils import change_revolute_to_fixed, change_revolute_to_fixed_regex from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path diff --git a/source/isaaclab/test/controllers/test_differential_ik.py b/source/isaaclab/test/controllers/test_differential_ik.py index 0b84e09eff2c..65ce828129f3 100644 --- a/source/isaaclab/test/controllers/test_differential_ik.py +++ b/source/isaaclab/test/controllers/test_differential_ik.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +12,9 @@ """Rest everything follows.""" +import pytest import torch -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils -import pytest from isaacsim.core.cloner import GridCloner import isaaclab.sim as sim_utils @@ -41,7 +39,7 @@ def sim(): """Create a simulation context for testing.""" # Wait for spawning - stage_utils.create_new_stage() + stage = sim_utils.create_new_stage() # Constants num_envs = 128 # Load kit helper @@ -59,7 +57,7 @@ def sim(): cloner.define_base_env("/World/envs") env_prim_paths = cloner.generate_paths("/World/envs/env", num_envs) # create source prim - prim_utils.define_prim(env_prim_paths[0], "Xform") + stage.DefinePrim(env_prim_paths[0], "Xform") # clone the env xform cloner.clone( source_prim_path=env_prim_paths[0], diff --git a/source/isaaclab/test/controllers/test_local_frame_task.py b/source/isaaclab/test/controllers/test_local_frame_task.py index 48c86eec0826..69790724248a 100644 --- a/source/isaaclab/test/controllers/test_local_frame_task.py +++ b/source/isaaclab/test/controllers/test_local_frame_task.py @@ -1,10 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Test cases for LocalFrameTask class.""" -# Import pinocchio in the main script to force the use of the dependencies installed by IsaacLab and not the one installed by Isaac Sim + +# Import pinocchio in the main script to force the use of the dependencies installed +# by IsaacLab and not the one installed by Isaac Sim # pinocchio is required by the Pink IK controller import sys @@ -16,9 +18,9 @@ # launch omniverse app simulation_app = AppLauncher(headless=True).app -import numpy as np from pathlib import Path +import numpy as np import pinocchio as pin import pytest diff --git a/source/isaaclab/test/controllers/test_null_space_posture_task.py b/source/isaaclab/test/controllers/test_null_space_posture_task.py index 97fc7221748a..a1d0e1ac50d5 100644 --- a/source/isaaclab/test/controllers/test_null_space_posture_task.py +++ b/source/isaaclab/test/controllers/test_null_space_posture_task.py @@ -1,9 +1,11 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Launch Isaac Sim Simulator first.""" -# Import pinocchio in the main script to force the use of the dependencies installed by IsaacLab and not the one installed by Isaac Sim + +# Import pinocchio in the main script to force the use of the dependencies installed +# by IsaacLab and not the one installed by Isaac Sim # pinocchio is required by the Pink IK controller import sys @@ -22,7 +24,6 @@ """Unit tests for NullSpacePostureTask with simplified robot configuration using Pink library directly.""" import numpy as np - import pytest from pink.configuration import Configuration from pink.tasks import FrameTask @@ -150,9 +151,9 @@ def test_null_space_jacobian_properties(self, robot_configuration, tasks, joint_ # Test: N * J^T should be approximately zero (null space property) # where N is the null space projector and J is the end-effector Jacobian null_space_projection = null_space_jacobian @ ee_jacobian.T - assert np.allclose( - null_space_projection, np.zeros_like(null_space_projection), atol=1e-7 - ), f"Null space projection of end-effector Jacobian not zero: {null_space_projection}" + assert np.allclose(null_space_projection, np.zeros_like(null_space_projection), atol=1e-7), ( + f"Null space projection of end-effector Jacobian not zero: {null_space_projection}" + ) def test_null_space_jacobian_identity_when_no_frame_tasks( self, robot_configuration, joint_configurations, num_joints @@ -173,9 +174,9 @@ def test_null_space_jacobian_identity_when_no_frame_tasks( # Should be identity matrix expected_identity = np.eye(num_joints) - assert np.allclose( - null_space_jacobian, expected_identity - ), f"Null space Jacobian should be identity when no frame tasks defined: {null_space_jacobian}" + assert np.allclose(null_space_jacobian, expected_identity), ( + f"Null space Jacobian should be identity when no frame tasks defined: {null_space_jacobian}" + ) def test_null_space_jacobian_consistency_across_configurations( self, robot_configuration, tasks, joint_configurations, num_joints @@ -215,9 +216,9 @@ def test_null_space_jacobian_consistency_across_configurations( null_space_velocity = jacobian @ random_velocity ee_velocity = ee_jacobian @ null_space_velocity - assert np.allclose( - ee_velocity, np.zeros(6), atol=1e-7 - ), f"End-effector velocity not zero for configuration {config}: {ee_velocity}" + assert np.allclose(ee_velocity, np.zeros(6), atol=1e-7), ( + f"End-effector velocity not zero for configuration {config}: {ee_velocity}" + ) def test_compute_error_without_target(self, robot_configuration, joint_configurations): """Test that compute_error raises ValueError when no target is set.""" @@ -258,14 +259,16 @@ def test_joint_masking(self, robot_configuration, joint_configurations, num_join error = null_space_task.compute_error(robot_configuration) # Only controlled joints should have non-zero error - # Joint indices: waist_yaw_joint=0, waist_pitch_joint=1, waist_roll_joint=2, left_shoulder_pitch_joint=3, left_shoulder_roll_joint=4, etc. + # Joint indices: + # waist_yaw_joint=0, waist_pitch_joint=1, waist_roll_joint=2, left_shoulder_pitch_joint=3, + # left_shoulder_roll_joint=4, etc. expected_error = np.zeros(num_joints) for i in joint_indexes: expected_error[i] = current_config[i] - assert np.allclose( - error, expected_error, atol=1e-7 - ), f"Joint mask not working correctly: expected {expected_error}, got {error}" + assert np.allclose(error, expected_error, atol=1e-7), ( + f"Joint mask not working correctly: expected {expected_error}, got {error}" + ) def test_empty_controlled_joints(self, robot_configuration, joint_configurations, num_joints): """Test behavior when controlled_joints is empty.""" @@ -331,9 +334,9 @@ def test_multiple_frame_tasks(self, robot_configuration, joint_configurations, n ee_velocity_left = jacobian_left_hand @ null_space_velocity ee_velocity_right = jacobian_right_hand @ null_space_velocity - assert np.allclose( - ee_velocity_left, np.zeros(6), atol=1e-7 - ), f"Left hand velocity not zero: {ee_velocity_left}" - assert np.allclose( - ee_velocity_right, np.zeros(6), atol=1e-7 - ), f"Right hand velocity not zero: {ee_velocity_right}" + assert np.allclose(ee_velocity_left, np.zeros(6), atol=1e-7), ( + f"Left hand velocity not zero: {ee_velocity_left}" + ) + assert np.allclose(ee_velocity_right, np.zeros(6), atol=1e-7), ( + f"Right hand velocity not zero: {ee_velocity_right}" + ) diff --git a/source/isaaclab/test/controllers/test_operational_space.py b/source/isaaclab/test/controllers/test_operational_space.py index b708b3572181..eeec14d877dc 100644 --- a/source/isaaclab/test/controllers/test_operational_space.py +++ b/source/isaaclab/test/controllers/test_operational_space.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +12,9 @@ """Rest everything follows.""" +import pytest import torch -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils -import pytest from isaacsim.core.cloner import GridCloner import isaaclab.sim as sim_utils @@ -45,7 +43,7 @@ def sim(): """Create a simulation context for testing.""" # Wait for spawning - stage_utils.create_new_stage() + stage = sim_utils.create_new_stage() # Constants num_envs = 16 # Load kit helper @@ -76,7 +74,7 @@ def sim(): cloner.define_base_env("/World/envs") env_prim_paths = cloner.generate_paths("/World/envs/env", num_envs) # create source prim - prim_utils.define_prim(env_prim_paths[0], "Xform") + stage.DefinePrim(env_prim_paths[0], "Xform") # clone the env xform cloner.clone( source_prim_path=env_prim_paths[0], @@ -200,7 +198,25 @@ def sim(): # Reference frame for targets frame = "root" - yield sim, num_envs, robot_cfg, ee_marker, goal_marker, contact_forces, target_abs_pos_set_b, target_abs_pose_set_b, target_rel_pos_set, target_rel_pose_set_b, target_abs_wrench_set, target_abs_pose_variable_kp_set, target_abs_pose_variable_set, target_hybrid_set_b, target_hybrid_variable_kp_set, target_hybrid_set_tilted, frame + yield ( + sim, + num_envs, + robot_cfg, + ee_marker, + goal_marker, + contact_forces, + target_abs_pos_set_b, + target_abs_pose_set_b, + target_rel_pos_set, + target_rel_pose_set_b, + target_abs_wrench_set, + target_abs_pose_variable_kp_set, + target_abs_pose_variable_set, + target_hybrid_set_b, + target_hybrid_variable_kp_set, + target_hybrid_set_tilted, + frame, + ) # Cleanup sim.stop() diff --git a/source/isaaclab/test/controllers/test_pink_ik.py b/source/isaaclab/test/controllers/test_pink_ik.py index 46f610c42f51..43ab48ae0590 100644 --- a/source/isaaclab/test/controllers/test_pink_ik.py +++ b/source/isaaclab/test/controllers/test_pink_ik.py @@ -1,10 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Launch Isaac Sim Simulator first.""" -# Import pinocchio in the main script to force the use of the dependencies installed by IsaacLab and not the one installed by Isaac Sim + +# Import pinocchio in the main script to force the use of the dependencies +# installed by IsaacLab and not the one installed by Isaac Sim # pinocchio is required by the Pink IK controller import sys @@ -19,18 +21,19 @@ """Rest everything follows.""" import contextlib -import gymnasium as gym import json -import numpy as np import re -import torch from pathlib import Path -import omni.usd +import gymnasium as gym +import numpy as np import pytest +import torch from pink.configuration import Configuration from pink.tasks import FrameTask +import omni.usd + from isaaclab.utils.math import axis_angle_from_quat, matrix_from_quat, quat_from_matrix, quat_inv import isaaclab_tasks # noqa: F401 @@ -110,9 +113,9 @@ def env_and_cfg(request): # Try to infer which is left and which is right left_candidates = [f for f in frames if "left" in f.lower()] right_candidates = [f for f in frames if "right" in f.lower()] - assert ( - len(left_candidates) == 1 and len(right_candidates) == 1 - ), f"Could not uniquely identify left/right frames from: {frames}" + assert len(left_candidates) == 1 and len(right_candidates) == 1, ( + f"Could not uniquely identify left/right frames from: {frames}" + ) left_eef_urdf_link_name = left_candidates[0] right_eef_urdf_link_name = right_candidates[0] diff --git a/source/isaaclab/test/controllers/test_pink_ik_components.py b/source/isaaclab/test/controllers/test_pink_ik_components.py index 6a691c353b2d..6bde4c30a1b6 100644 --- a/source/isaaclab/test/controllers/test_pink_ik_components.py +++ b/source/isaaclab/test/controllers/test_pink_ik_components.py @@ -1,10 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Test cases for PinkKinematicsConfiguration class.""" -# Import pinocchio in the main script to force the use of the dependencies installed by IsaacLab and not the one installed by Isaac Sim + +# Import pinocchio in the main script to force the use of the dependencies installed +# by IsaacLab and not the one installed by Isaac Sim # pinocchio is required by the Pink IK controller import sys @@ -16,9 +18,9 @@ # launch omniverse app simulation_app = AppLauncher(headless=True).app -import numpy as np from pathlib import Path +import numpy as np import pinocchio as pin import pytest from pink.exceptions import FrameNotFound diff --git a/source/isaaclab/test/deps/isaacsim/check_camera.py b/source/isaaclab/test/deps/isaacsim/check_camera.py index c9e0374fc92d..81f481f23e3f 100644 --- a/source/isaaclab/test/deps/isaacsim/check_camera.py +++ b/source/isaaclab/test/deps/isaacsim/check_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -41,22 +41,18 @@ """Rest everything follows.""" -import numpy as np import os import random -try: - import isaacsim.storage.native as nucleus_utils -except ModuleNotFoundError: - import isaacsim.core.utils.nucleus as nucleus_utils +import numpy as np +from PIL import Image, ImageChops +import isaacsim.core.utils.nucleus as nucleus_utils import isaacsim.core.utils.prims as prim_utils import omni.replicator.core as rep from isaacsim.core.api.world import World from isaacsim.core.prims import Articulation, RigidPrim, SingleGeometryPrim, SingleRigidPrim -from isaacsim.core.utils.carb import set_carb_setting from isaacsim.core.utils.viewports import set_camera_view -from PIL import Image, ImageChops from pxr import Gf, UsdGeom # check nucleus connection @@ -85,7 +81,7 @@ def main(): world.get_physics_context().enable_flatcache(True) # Enable hydra scene-graph instancing # this is needed to visualize the scene when flatcache is enabled - set_carb_setting(world._settings, "/persistent/omnihydra/useSceneGraphInstancing", True) + world._settings.set_bool("/persistent/omnihydra/useSceneGraphInstancing", True) # Populate scene # Ground diff --git a/source/isaaclab/test/deps/isaacsim/check_floating_base_made_fixed.py b/source/isaaclab/test/deps/isaacsim/check_floating_base_made_fixed.py index dbe12e8265f7..0be9a55bd4cb 100644 --- a/source/isaaclab/test/deps/isaacsim/check_floating_base_made_fixed.py +++ b/source/isaaclab/test/deps/isaacsim/check_floating_base_made_fixed.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,27 +30,31 @@ """Rest everything follows.""" +import logging + import torch import isaacsim.core.utils.nucleus as nucleus_utils import isaacsim.core.utils.prims as prim_utils import isaacsim.core.utils.stage as stage_utils import omni.kit.commands -import omni.log import omni.physx from isaacsim.core.api.world import World from isaacsim.core.prims import Articulation -from isaacsim.core.utils.carb import set_carb_setting from isaacsim.core.utils.viewports import set_camera_view from pxr import PhysxSchema, UsdPhysics +# import logger +logger = logging.getLogger(__name__) + + # check nucleus connection if nucleus_utils.get_assets_root_path() is None: msg = ( "Unable to perform Nucleus login on Omniverse. Assets root path is not set.\n" "\tPlease check: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html#omniverse-nucleus" ) - omni.log.error(msg) + logger.error(msg) raise RuntimeError(msg) @@ -75,7 +79,7 @@ def main(): # Enable hydra scene-graph instancing # this is needed to visualize the scene when flatcache is enabled - set_carb_setting(world._settings, "/persistent/omnihydra/useSceneGraphInstancing", True) + world._settings.set_bool("/persistent/omnihydra/useSceneGraphInstancing", True) # Spawn things into stage # Ground-plane diff --git a/source/isaaclab/test/deps/isaacsim/check_legged_robot_clone.py b/source/isaaclab/test/deps/isaacsim/check_legged_robot_clone.py index c26f627a2204..57c016d7522d 100644 --- a/source/isaaclab/test/deps/isaacsim/check_legged_robot_clone.py +++ b/source/isaaclab/test/deps/isaacsim/check_legged_robot_clone.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,30 +40,28 @@ """Rest everything follows.""" +import logging import os -import torch - -import omni.log -try: - import isaacsim.storage.native as nucleus_utils -except ModuleNotFoundError: - import isaacsim.core.utils.nucleus as nucleus_utils +import torch +import isaacsim.core.utils.nucleus as nucleus_utils import isaacsim.core.utils.prims as prim_utils from isaacsim.core.api.world import World from isaacsim.core.cloner import GridCloner from isaacsim.core.prims import Articulation -from isaacsim.core.utils.carb import set_carb_setting from isaacsim.core.utils.viewports import set_camera_view +# import logger +logger = logging.getLogger(__name__) + # check nucleus connection if nucleus_utils.get_assets_root_path() is None: msg = ( "Unable to perform Nucleus login on Omniverse. Assets root path is not set.\n" "\tPlease check: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html#omniverse-nucleus" ) - omni.log.error(msg) + logger.error(msg) raise RuntimeError(msg) @@ -89,7 +87,7 @@ def main(): # Enable hydra scene-graph instancing # this is needed to visualize the scene when flatcache is enabled - set_carb_setting(world._settings, "/persistent/omnihydra/useSceneGraphInstancing", True) + world._settings.set_bool("/persistent/omnihydra/useSceneGraphInstancing", True) # Create interface to clone the scene cloner = GridCloner(spacing=2.0) diff --git a/source/isaaclab/test/deps/isaacsim/check_ref_count.py b/source/isaaclab/test/deps/isaacsim/check_ref_count.py index 2683bd3e9897..7927b8cb01a1 100644 --- a/source/isaaclab/test/deps/isaacsim/check_ref_count.py +++ b/source/isaaclab/test/deps/isaacsim/check_ref_count.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,19 +35,18 @@ import ctypes import gc -import torch # noqa: F401 - -import omni.log +import logging -try: - import isaacsim.storage.native as nucleus_utils -except ModuleNotFoundError: - import isaacsim.core.utils.nucleus as nucleus_utils +import torch # noqa: F401 +import isaacsim.core.utils.nucleus as nucleus_utils import isaacsim.core.utils.prims as prim_utils from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.prims import Articulation -from isaacsim.core.utils.carb import set_carb_setting + +# import logger +logger = logging.getLogger(__name__) + # check nucleus connection if nucleus_utils.get_assets_root_path() is None: @@ -55,7 +54,7 @@ "Unable to perform Nucleus login on Omniverse. Assets root path is not set.\n" "\tPlease check: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html#omniverse-nucleus" ) - omni.log.error(msg) + logger.error(msg) raise RuntimeError(msg) @@ -110,7 +109,7 @@ def main(): # Enable hydra scene-graph instancing # this is needed to visualize the scene when flatcache is enabled - set_carb_setting(sim._settings, "/persistent/omnihydra/useSceneGraphInstancing", True) + sim._settings.set_bool("/persistent/omnihydra/useSceneGraphInstancing", True) # Create a dummy tensor for testing # Uncommenting the following line will yield a reference count of 1 for the robot (as desired) diff --git a/source/isaaclab/test/deps/isaacsim/check_rep_texture_randomizer.py b/source/isaaclab/test/deps/isaacsim/check_rep_texture_randomizer.py index 5700b5c9044f..170bf1b9683c 100644 --- a/source/isaaclab/test/deps/isaacsim/check_rep_texture_randomizer.py +++ b/source/isaaclab/test/deps/isaacsim/check_rep_texture_randomizer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -47,9 +47,9 @@ import isaacsim.core.utils.prims as prim_utils import omni.replicator.core as rep +from isaacsim.core.api.objects import DynamicSphere from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.cloner import GridCloner -from isaacsim.core.objects import DynamicSphere from isaacsim.core.prims import RigidPrim from isaacsim.core.utils.viewports import set_camera_view diff --git a/source/isaaclab/test/deps/test_scipy.py b/source/isaaclab/test/deps/test_scipy.py index 9cc33b37a842..d697716aad7a 100644 --- a/source/isaaclab/test/deps/test_scipy.py +++ b/source/isaaclab/test/deps/test_scipy.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/deps/test_torch.py b/source/isaaclab/test/deps/test_torch.py index fcdf746e76b2..6a50110757de 100644 --- a/source/isaaclab/test/deps/test_torch.py +++ b/source/isaaclab/test/deps/test_torch.py @@ -1,13 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 pytest import torch import torch.utils.benchmark as benchmark -import pytest - @pytest.mark.isaacsim_ci def test_array_slicing(): diff --git a/source/isaaclab/test/devices/check_keyboard.py b/source/isaaclab/test/devices/check_keyboard.py index 711423d3e5e6..c1a8b07bef82 100644 --- a/source/isaaclab/test/devices/check_keyboard.py +++ b/source/isaaclab/test/devices/check_keyboard.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -23,9 +23,8 @@ import ctypes -from isaacsim.core.api.simulation_context import SimulationContext - from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg +from isaaclab.sim import SimulationCfg, SimulationContext def print_cb(): @@ -41,7 +40,7 @@ def quit_cb(): def main(): # Load kit helper - sim = SimulationContext(physics_dt=0.01, rendering_dt=0.01) + sim = SimulationContext(SimulationCfg(dt=0.01)) # Create teleoperation interface teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1)) diff --git a/source/isaaclab/test/devices/test_device_constructors.py b/source/isaaclab/test/devices/test_device_constructors.py index ffe44740d01d..fb1bded4d61a 100644 --- a/source/isaaclab/test/devices/test_device_constructors.py +++ b/source/isaaclab/test/devices/test_device_constructors.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,12 +13,17 @@ """Rest everything follows.""" import importlib -import torch +import json +from typing import cast import pytest +import torch # Import device classes to test from isaaclab.devices import ( + DeviceCfg, + HaplyDevice, + HaplyDeviceCfg, OpenXRDevice, OpenXRDeviceCfg, Se2Gamepad, @@ -66,6 +71,11 @@ def mock_environment(mocker): carb_mock.input.KeyboardEventType.KEY_PRESS = 1 carb_mock.input.KeyboardEventType.KEY_RELEASE = 2 + # Mock carb events used by OpenXRDevice + events_mock = mocker.MagicMock() + events_mock.type_from_string.return_value = 0 + carb_mock.events = events_mock + # Mock the SpaceMouse hid_mock.enumerate.return_value = [{"product_string": "SpaceMouse Compact", "vendor_id": 123, "product_id": 456}] hid_mock.device.return_value = device_mock @@ -79,6 +89,11 @@ def mock_environment(mocker): omni_mock.kit.xr.core.XRPoseValidityFlags.POSITION_VALID = 1 omni_mock.kit.xr.core.XRPoseValidityFlags.ORIENTATION_VALID = 2 + # Mock Haply WebSocket + websockets_mock = mocker.MagicMock() + websocket_mock = mocker.MagicMock() + websockets_mock.connect.return_value.__aenter__.return_value = websocket_mock + return { "carb": carb_mock, "omni": omni_mock, @@ -89,6 +104,8 @@ def mock_environment(mocker): "settings": settings_mock, "hid": hid_mock, "device": device_mock, + "websockets": websockets_mock, + "websocket": websocket_mock, } @@ -290,6 +307,7 @@ def test_openxr_constructors(mock_environment, mocker): "isaacsim.core.prims": mocker.MagicMock(), }, ) + mocker.patch.object(device_mod, "carb", mock_environment["carb"]) mocker.patch.object(device_mod, "XRCore", mock_environment["omni"].kit.xr.core.XRCore) mocker.patch.object(device_mod, "XRPoseValidityFlags", mock_environment["omni"].kit.xr.core.XRPoseValidityFlags) mock_single_xform = mocker.patch.object(device_mod, "SingleXFormPrim") @@ -321,6 +339,145 @@ def test_openxr_constructors(mock_environment, mocker): device.reset() +""" +Test Haply devices. +""" + + +def test_haply_constructors(mock_environment, mocker): + """Test constructor for HaplyDevice.""" + # Test config-based constructor + config = HaplyDeviceCfg( + websocket_uri="ws://localhost:10001", + pos_sensitivity=1.5, + data_rate=250.0, + ) + + # Mock the websockets module and asyncio + device_mod = importlib.import_module("isaaclab.devices.haply.se3_haply") + mocker.patch.dict("sys.modules", {"websockets": mock_environment["websockets"]}) + mocker.patch.object(device_mod, "websockets", mock_environment["websockets"]) + + # Mock asyncio to prevent actual async operations + asyncio_mock = mocker.MagicMock() + mocker.patch.object(device_mod, "asyncio", asyncio_mock) + + # Mock threading to prevent actual thread creation + threading_mock = mocker.MagicMock() + thread_instance = mocker.MagicMock() + threading_mock.Thread.return_value = thread_instance + thread_instance.is_alive.return_value = False + mocker.patch.object(device_mod, "threading", threading_mock) + + # Mock time.time() for connection timeout simulation + time_mock = mocker.MagicMock() + time_mock.time.side_effect = [0.0, 0.1, 0.2, 0.3, 6.0] # Will timeout + mocker.patch.object(device_mod, "time", time_mock) + + # Create sample WebSocket response data + ws_response = { + "inverse3": [ + { + "device_id": "test_inverse3_123", + "state": {"cursor_position": {"x": 0.1, "y": 0.2, "z": 0.3}}, + } + ], + "wireless_verse_grip": [ + { + "device_id": "test_versegrip_456", + "state": { + "orientation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0}, + "buttons": {"a": False, "b": False, "c": False}, + }, + } + ], + } + + # Configure websocket mock to return JSON data + mock_environment["websocket"].recv = mocker.AsyncMock(return_value=json.dumps(ws_response)) + mock_environment["websocket"].send = mocker.AsyncMock() + + # The constructor will raise RuntimeError due to timeout, which is expected in test + with pytest.raises(RuntimeError, match="Failed to connect both Inverse3 and VerseGrip devices"): + haply = HaplyDevice(config) + + # Now test successful connection by mocking time to not timeout + time_mock.time.side_effect = [0.0, 0.1, 0.2, 0.3, 0.4] # Won't timeout + + # Mock the connection status + mocker.patch.object(device_mod.HaplyDevice, "_start_websocket_thread") + haply = device_mod.HaplyDevice.__new__(device_mod.HaplyDevice) + haply._sim_device = config.sim_device + haply.websocket_uri = config.websocket_uri + haply.pos_sensitivity = config.pos_sensitivity + haply.data_rate = config.data_rate + haply.limit_force = config.limit_force + haply.connected = True + haply.inverse3_device_id = "test_inverse3_123" + haply.verse_grip_device_id = "test_versegrip_456" + haply.data_lock = threading_mock.Lock() + haply.force_lock = threading_mock.Lock() + haply._connected_lock = threading_mock.Lock() + haply._additional_callbacks = {} + haply._prev_buttons = {"a": False, "b": False, "c": False} + haply._websocket_thread = None # Initialize to prevent AttributeError in __del__ + haply.running = True + haply.cached_data = { + "position": torch.tensor([0.1, 0.2, 0.3], dtype=torch.float32).numpy(), + "quaternion": torch.tensor([0.0, 0.0, 0.0, 1.0], dtype=torch.float32).numpy(), + "buttons": {"a": False, "b": False, "c": False}, + "inverse3_connected": True, + "versegrip_connected": True, + } + haply.feedback_force = {"x": 0.0, "y": 0.0, "z": 0.0} + + # Verify configuration was applied correctly + assert haply.websocket_uri == "ws://localhost:10001" + assert haply.pos_sensitivity == 1.5 + assert haply.data_rate == 250.0 + + # Test advance() returns expected type + result = haply.advance() + assert isinstance(result, torch.Tensor) + assert result.shape == (10,) # (pos_x, pos_y, pos_z, qx, qy, qz, qw, btn_a, btn_b, btn_c) + + # Test push_force with tensor (single force vector) + forces_within = torch.tensor([[1.0, 1.5, -0.5]], dtype=torch.float32) + position_zero = torch.tensor([0], dtype=torch.long) + haply.push_force(forces_within, position_zero) + assert haply.feedback_force["x"] == pytest.approx(1.0) + assert haply.feedback_force["y"] == pytest.approx(1.5) + assert haply.feedback_force["z"] == pytest.approx(-0.5) + + # Test push_force with tensor (force limiting, default limit is 2.0 N) + forces_exceed = torch.tensor([[5.0, -10.0, 1.5]], dtype=torch.float32) + haply.push_force(forces_exceed, position_zero) + assert haply.feedback_force["x"] == pytest.approx(2.0) + assert haply.feedback_force["y"] == pytest.approx(-2.0) + assert haply.feedback_force["z"] == pytest.approx(1.5) + + # Test push_force with position tensor (single index) + forces_multi = torch.tensor([[1.0, 2.0, 3.0], [0.5, 0.8, -0.3], [0.1, 0.2, 0.3]], dtype=torch.float32) + position_single = torch.tensor([1], dtype=torch.long) + haply.push_force(forces_multi, position=position_single) + assert haply.feedback_force["x"] == pytest.approx(0.5) + assert haply.feedback_force["y"] == pytest.approx(0.8) + assert haply.feedback_force["z"] == pytest.approx(-0.3) + + # Test push_force with position tensor (multiple indices) + position_multi = torch.tensor([0, 2], dtype=torch.long) + haply.push_force(forces_multi, position=position_multi) + # Should sum forces[0] and forces[2]: [1.0+0.1, 2.0+0.2, 3.0+0.3] = [1.1, 2.2, 3.3] + # But clipped to [-2.0, 2.0]: [1.1, 2.0, 2.0] + assert haply.feedback_force["x"] == pytest.approx(1.1) + assert haply.feedback_force["y"] == pytest.approx(2.0) + assert haply.feedback_force["z"] == pytest.approx(2.0) + + # Test reset functionality + haply.reset() + assert haply.feedback_force == {"x": 0.0, "y": 0.0, "z": 0.0} + + """ Test teleop device factory. """ @@ -332,7 +489,7 @@ def test_create_teleop_device_basic(mock_environment, mocker): keyboard_cfg = Se3KeyboardCfg(pos_sensitivity=0.8, rot_sensitivity=1.2) # Create devices configuration dictionary - devices_cfg = {"test_keyboard": keyboard_cfg} + devices_cfg: dict[str, DeviceCfg] = {"test_keyboard": keyboard_cfg} # Mock Se3Keyboard class device_mod = importlib.import_module("isaaclab.devices.keyboard.se3_keyboard") @@ -356,7 +513,7 @@ def test_create_teleop_device_with_callbacks(mock_environment, mocker): openxr_cfg = OpenXRDeviceCfg(xr_cfg=xr_cfg) # Create devices configuration dictionary - devices_cfg = {"test_xr": openxr_cfg} + devices_cfg: dict[str, DeviceCfg] = {"test_xr": openxr_cfg} # Create mock callbacks button_a_callback = mocker.MagicMock() @@ -373,6 +530,7 @@ def test_create_teleop_device_with_callbacks(mock_environment, mocker): "isaacsim.core.prims": mocker.MagicMock(), }, ) + mocker.patch.object(device_mod, "carb", mock_environment["carb"]) mocker.patch.object(device_mod, "XRCore", mock_environment["omni"].kit.xr.core.XRCore) mocker.patch.object(device_mod, "XRPoseValidityFlags", mock_environment["omni"].kit.xr.core.XRPoseValidityFlags) mock_single_xform = mocker.patch.object(device_mod, "SingleXFormPrim") @@ -387,10 +545,8 @@ def test_create_teleop_device_with_callbacks(mock_environment, mocker): # Verify the device was created correctly assert isinstance(device, OpenXRDevice) - # Verify callbacks were registered - device.add_callback("button_a", button_a_callback) - device.add_callback("button_b", button_b_callback) - assert len(device._additional_callbacks) == 2 + # Verify callbacks were registered by the factory + assert set(device._additional_callbacks.keys()) == {"button_a", "button_b"} def test_create_teleop_device_with_retargeters(mock_environment, mocker): @@ -404,7 +560,7 @@ def test_create_teleop_device_with_retargeters(mock_environment, mocker): device_cfg = OpenXRDeviceCfg(xr_cfg=xr_cfg, retargeters=[retargeter_cfg1, retargeter_cfg2]) # Create devices configuration dictionary - devices_cfg = {"test_xr": device_cfg} + devices_cfg: dict[str, DeviceCfg] = {"test_xr": device_cfg} # Mock OpenXRDevice class and dependencies device_mod = importlib.import_module("isaaclab.devices.openxr.openxr_device") @@ -416,6 +572,7 @@ def test_create_teleop_device_with_retargeters(mock_environment, mocker): "isaacsim.core.prims": mocker.MagicMock(), }, ) + mocker.patch.object(device_mod, "carb", mock_environment["carb"]) mocker.patch.object(device_mod, "XRCore", mock_environment["omni"].kit.xr.core.XRCore) mocker.patch.object(device_mod, "XRPoseValidityFlags", mock_environment["omni"].kit.xr.core.XRPoseValidityFlags) mock_single_xform = mocker.patch.object(device_mod, "SingleXFormPrim") @@ -424,11 +581,6 @@ def test_create_teleop_device_with_retargeters(mock_environment, mocker): mock_instance = mock_single_xform.return_value mock_instance.prim_path = "/XRAnchor" - # Mock retargeter classes - retargeter_mod = importlib.import_module("isaaclab.devices.openxr.retargeters") - mocker.patch.object(retargeter_mod, "Se3AbsRetargeter") - mocker.patch.object(retargeter_mod, "GripperRetargeter") - # Create the device using the factory device = create_teleop_device("test_xr", devices_cfg) @@ -439,7 +591,7 @@ def test_create_teleop_device_with_retargeters(mock_environment, mocker): def test_create_teleop_device_device_not_found(): """Test error when device name is not found in configuration.""" # Create devices configuration dictionary - devices_cfg = {"keyboard": Se3KeyboardCfg()} + devices_cfg: dict[str, DeviceCfg] = {"keyboard": Se3KeyboardCfg()} # Try to create a non-existent device with pytest.raises(ValueError, match="Device 'gamepad' not found"): @@ -454,8 +606,8 @@ class UnsupportedCfg: pass # Create devices configuration dictionary with unsupported config - devices_cfg = {"unsupported": UnsupportedCfg()} + devices_cfg: dict[str, DeviceCfg] = cast(dict[str, DeviceCfg], {"unsupported": UnsupportedCfg()}) # Try to create a device with unsupported configuration - with pytest.raises(ValueError, match="Unsupported device configuration type"): + with pytest.raises(ValueError, match="does not declare class_type"): create_teleop_device("unsupported", devices_cfg) diff --git a/source/isaaclab/test/devices/test_oxr_device.py b/source/isaaclab/test/devices/test_oxr_device.py index 6bf805d32727..6402d8e0c187 100644 --- a/source/isaaclab/test/devices/test_oxr_device.py +++ b/source/isaaclab/test/devices/test_oxr_device.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,20 +18,41 @@ simulation_app = app_launcher.app import importlib + import numpy as np +import pytest +import torch import carb import omni.usd -import pytest from isaacsim.core.prims import XFormPrim from isaaclab.devices import OpenXRDevice, OpenXRDeviceCfg from isaaclab.devices.openxr import XrCfg +from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass +class NoOpRetargeter(RetargeterBase): + """A no-op retargeter that requests hand and head tracking but returns empty tensor.""" + + def __init__(self, cfg: RetargeterCfg): + super().__init__(cfg) + + def get_requirements(self) -> list[RetargeterBase.Requirement]: + """Request hand and head tracking to trigger data collection.""" + return [ + RetargeterBase.Requirement.HAND_TRACKING, + RetargeterBase.Requirement.HEAD_TRACKING, + ] + + def retarget(self, data): + """Return empty tensor.""" + return torch.tensor([], device=self._sim_device) + + @configclass class EmptyManagerCfg: """Empty manager.""" @@ -159,7 +180,7 @@ def test_xr_anchor(empty_env, mock_xrcore): device = OpenXRDevice(OpenXRDeviceCfg(xr_cfg=env_cfg.xr)) # Check that the xr anchor prim is created with the correct pose - xr_anchor_prim = XFormPrim("/XRAnchor") + xr_anchor_prim = XFormPrim("/World/XRAnchor") assert xr_anchor_prim.is_valid() position, orientation = xr_anchor_prim.get_world_poses() @@ -168,7 +189,7 @@ def test_xr_anchor(empty_env, mock_xrcore): # Check that xr anchor mode and custom anchor are set correctly assert carb.settings.get_settings().get("/persistent/xr/profile/ar/anchorMode") == "custom anchor" - assert carb.settings.get_settings().get("/xrstage/profile/ar/customAnchor") == "/XRAnchor" + assert carb.settings.get_settings().get("/xrstage/profile/ar/customAnchor") == "/World/XRAnchor" device.reset() @@ -181,7 +202,7 @@ def test_xr_anchor_default(empty_env, mock_xrcore): device = OpenXRDevice(OpenXRDeviceCfg()) # Check that the xr anchor prim is created with the correct default pose - xr_anchor_prim = XFormPrim("/XRAnchor") + xr_anchor_prim = XFormPrim("/World/XRAnchor") assert xr_anchor_prim.is_valid() position, orientation = xr_anchor_prim.get_world_poses() @@ -190,7 +211,7 @@ def test_xr_anchor_default(empty_env, mock_xrcore): # Check that xr anchor mode and custom anchor are set correctly assert carb.settings.get_settings().get("/persistent/xr/profile/ar/anchorMode") == "custom anchor" - assert carb.settings.get_settings().get("/xrstage/profile/ar/customAnchor") == "/XRAnchor" + assert carb.settings.get_settings().get("/xrstage/profile/ar/customAnchor") == "/World/XRAnchor" device.reset() @@ -204,7 +225,7 @@ def test_xr_anchor_multiple_devices(empty_env, mock_xrcore): device_2 = OpenXRDevice(OpenXRDeviceCfg()) # Check that the xr anchor prim is created with the correct default pose - xr_anchor_prim = XFormPrim("/XRAnchor") + xr_anchor_prim = XFormPrim("/World/XRAnchor") assert xr_anchor_prim.is_valid() position, orientation = xr_anchor_prim.get_world_poses() @@ -213,7 +234,7 @@ def test_xr_anchor_multiple_devices(empty_env, mock_xrcore): # Check that xr anchor mode and custom anchor are set correctly assert carb.settings.get_settings().get("/persistent/xr/profile/ar/anchorMode") == "custom anchor" - assert carb.settings.get_settings().get("/xrstage/profile/ar/customAnchor") == "/XRAnchor" + assert carb.settings.get_settings().get("/xrstage/profile/ar/customAnchor") == "/World/XRAnchor" device_1.reset() device_2.reset() @@ -223,19 +244,22 @@ def test_xr_anchor_multiple_devices(empty_env, mock_xrcore): def test_get_raw_data(empty_env, mock_xrcore): """Test the _get_raw_data method returns correctly formatted tracking data.""" env, _ = empty_env - # Create a proper config object with default values - device = OpenXRDevice(OpenXRDeviceCfg()) + # Create a proper config object with default values and a no-op retargeter to trigger data collection + retargeter = NoOpRetargeter(RetargeterCfg()) + device = OpenXRDevice(OpenXRDeviceCfg(), retargeters=[retargeter]) # Get raw tracking data raw_data = device._get_raw_data() # Check that the data structure is as expected - assert OpenXRDevice.TrackingTarget.HAND_LEFT in raw_data - assert OpenXRDevice.TrackingTarget.HAND_RIGHT in raw_data - assert OpenXRDevice.TrackingTarget.HEAD in raw_data + from isaaclab.devices.device_base import DeviceBase + + assert DeviceBase.TrackingTarget.HAND_LEFT in raw_data + assert DeviceBase.TrackingTarget.HAND_RIGHT in raw_data + assert DeviceBase.TrackingTarget.HEAD in raw_data # Check left hand joints - left_hand = raw_data[OpenXRDevice.TrackingTarget.HAND_LEFT] + left_hand = raw_data[DeviceBase.TrackingTarget.HAND_LEFT] assert "palm" in left_hand assert "wrist" in left_hand @@ -246,7 +270,7 @@ def test_get_raw_data(empty_env, mock_xrcore): np.testing.assert_almost_equal(palm_pose[3:], [0.9, 0.1, 0.2, 0.3]) # Orientation # Check head pose - head_pose = raw_data[OpenXRDevice.TrackingTarget.HEAD] + head_pose = raw_data[DeviceBase.TrackingTarget.HEAD] assert len(head_pose) == 7 np.testing.assert_almost_equal(head_pose[:3], [0.1, 0.2, 0.3]) # Position np.testing.assert_almost_equal(head_pose[3:], [0.9, 0.1, 0.2, 0.3]) # Orientation diff --git a/source/isaaclab/test/devices/test_retargeters.py b/source/isaaclab/test/devices/test_retargeters.py new file mode 100644 index 000000000000..c080c4a43d9c --- /dev/null +++ b/source/isaaclab/test/devices/test_retargeters.py @@ -0,0 +1,370 @@ +# 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 + +""" +Unit tests for retargeters. +""" + +from isaaclab.app import AppLauncher + +# Can set this to False to see the GUI for debugging. +HEADLESS = True + +# Launch omniverse app. +app_launcher = AppLauncher(headless=HEADLESS) +simulation_app = app_launcher.app + +import sys +import unittest +from unittest.mock import MagicMock, patch + +import numpy as np +import torch + +# Mock dependencies that might require a running simulation or specific hardware +sys.modules["isaaclab.markers"] = MagicMock() +sys.modules["isaaclab.markers.config"] = MagicMock() +sys.modules["isaaclab.sim"] = MagicMock() +sys.modules["isaaclab.sim.SimulationContext"] = MagicMock() + +# Mock SimulationContext instance +mock_sim_context = MagicMock() +mock_sim_context.get_rendering_dt.return_value = 0.016 # 60Hz +sys.modules["isaaclab.sim"].SimulationContext.instance.return_value = mock_sim_context + + +# Import after mocking +from isaaclab.devices.device_base import DeviceBase +from isaaclab.devices.openxr.retargeters.humanoid.unitree.g1_lower_body_standing import ( + G1LowerBodyStandingRetargeter, + G1LowerBodyStandingRetargeterCfg, +) +from isaaclab.devices.openxr.retargeters.humanoid.unitree.g1_motion_controller_locomotion import ( + G1LowerBodyStandingMotionControllerRetargeter, + G1LowerBodyStandingMotionControllerRetargeterCfg, +) +from isaaclab.devices.openxr.retargeters.manipulator.gripper_retargeter import GripperRetargeter, GripperRetargeterCfg +from isaaclab.devices.openxr.retargeters.manipulator.se3_abs_retargeter import Se3AbsRetargeter, Se3AbsRetargeterCfg +from isaaclab.devices.openxr.retargeters.manipulator.se3_rel_retargeter import Se3RelRetargeter, Se3RelRetargeterCfg + +# Mock dex retargeting utils +with patch.dict( + sys.modules, + { + "isaaclab.devices.openxr.retargeters.humanoid.unitree.inspire.g1_dex_retargeting_utils": MagicMock(), + "isaaclab.devices.openxr.retargeters.humanoid.fourier.gr1_t2_dex_retargeting_utils": MagicMock(), + "isaaclab.devices.openxr.retargeters.humanoid.unitree.trihand.g1_dex_retargeting_utils": MagicMock(), + }, +): + from isaaclab.devices.openxr.retargeters.humanoid.fourier.gr1t2_retargeter import ( + GR1T2Retargeter, + GR1T2RetargeterCfg, + ) + from isaaclab.devices.openxr.retargeters.humanoid.unitree.inspire.g1_upper_body_retargeter import ( + UnitreeG1Retargeter, + UnitreeG1RetargeterCfg, + ) + from isaaclab.devices.openxr.retargeters.humanoid.unitree.trihand.g1_upper_body_motion_ctrl_gripper import ( + G1TriHandUpperBodyMotionControllerGripperRetargeter, + G1TriHandUpperBodyMotionControllerGripperRetargeterCfg, + ) + from isaaclab.devices.openxr.retargeters.humanoid.unitree.trihand.g1_upper_body_motion_ctrl_retargeter import ( + G1TriHandUpperBodyMotionControllerRetargeter, + G1TriHandUpperBodyMotionControllerRetargeterCfg, + ) + from isaaclab.devices.openxr.retargeters.humanoid.unitree.trihand.g1_upper_body_retargeter import ( + G1TriHandUpperBodyRetargeter, + G1TriHandUpperBodyRetargeterCfg, + ) + + +class TestSe3AbsRetargeter(unittest.TestCase): + def setUp(self): + self.cfg = Se3AbsRetargeterCfg( + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, enable_visualization=False, sim_device="cpu" + ) + self.retargeter = Se3AbsRetargeter(self.cfg) + + def test_retarget_defaults(self): + # Mock input data + wrist_pose = np.array([0.1, 0.2, 0.3, 1.0, 0.0, 0.0, 0.0]) + thumb_tip_pose = np.array([0.15, 0.25, 0.35, 1.0, 0.0, 0.0, 0.0]) + index_tip_pose = np.array([0.15, 0.20, 0.35, 1.0, 0.0, 0.0, 0.0]) + + data = { + DeviceBase.TrackingTarget.HAND_RIGHT: { + "wrist": wrist_pose, + "thumb_tip": thumb_tip_pose, + "index_tip": index_tip_pose, + } + } + + result = self.retargeter.retarget(data) + + self.assertIsInstance(result, torch.Tensor) + self.assertEqual(result.shape, (7,)) + np.testing.assert_allclose(result[:3].numpy(), wrist_pose[:3], rtol=1e-5) + self.assertAlmostEqual(torch.norm(result[3:]).item(), 1.0, places=4) + + def test_pinch_position(self): + self.cfg.use_wrist_position = False + retargeter = Se3AbsRetargeter(self.cfg) + + wrist_pose = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + thumb_tip_pose = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + index_tip_pose = np.array([3.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + data = { + DeviceBase.TrackingTarget.HAND_RIGHT: { + "wrist": wrist_pose, + "thumb_tip": thumb_tip_pose, + "index_tip": index_tip_pose, + } + } + + result = retargeter.retarget(data) + expected_pos = np.array([2.0, 0.0, 0.0]) + np.testing.assert_allclose(result[:3].numpy(), expected_pos, rtol=1e-5) + + +class TestSe3RelRetargeter(unittest.TestCase): + def setUp(self): + self.cfg = Se3RelRetargeterCfg( + bound_hand=DeviceBase.TrackingTarget.HAND_LEFT, + enable_visualization=False, + sim_device="cpu", + delta_pos_scale_factor=1.0, + delta_rot_scale_factor=1.0, + alpha_pos=1.0, + alpha_rot=1.0, + ) + self.retargeter = Se3RelRetargeter(self.cfg) + + def test_retarget_movement(self): + wrist_pose_1 = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + thumb_tip_pose_1 = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + index_tip_pose_1 = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + data_1 = { + DeviceBase.TrackingTarget.HAND_LEFT: { + "wrist": wrist_pose_1, + "thumb_tip": thumb_tip_pose_1, + "index_tip": index_tip_pose_1, + } + } + + _ = self.retargeter.retarget(data_1) + + wrist_pose_2 = np.array([0.1, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + thumb_tip_pose_2 = np.array([0.1, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + index_tip_pose_2 = np.array([0.1, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + data_2 = { + DeviceBase.TrackingTarget.HAND_LEFT: { + "wrist": wrist_pose_2, + "thumb_tip": thumb_tip_pose_2, + "index_tip": index_tip_pose_2, + } + } + + result = self.retargeter.retarget(data_2) + self.assertEqual(result.shape, (6,)) + np.testing.assert_allclose(result[:3].numpy(), [0.1, 0.0, 0.0], rtol=1e-4) + + +class TestGripperRetargeter(unittest.TestCase): + def setUp(self): + self.cfg = GripperRetargeterCfg(bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, sim_device="cpu") + self.retargeter = GripperRetargeter(self.cfg) + + def test_gripper_logic(self): + data_open = { + DeviceBase.TrackingTarget.HAND_RIGHT: { + "thumb_tip": np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]), + "index_tip": np.array([0.1, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]), + } + } + result = self.retargeter.retarget(data_open) + self.assertEqual(result.item(), 1.0) + + data_close = { + DeviceBase.TrackingTarget.HAND_RIGHT: { + "thumb_tip": np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]), + "index_tip": np.array([0.02, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]), + } + } + result = self.retargeter.retarget(data_close) + self.assertEqual(result.item(), -1.0) + + +class TestG1LowerBodyStandingRetargeter(unittest.TestCase): + def test_retarget(self): + cfg = G1LowerBodyStandingRetargeterCfg(hip_height=0.8, sim_device="cpu") + retargeter = G1LowerBodyStandingRetargeter(cfg) + result = retargeter.retarget({}) + self.assertTrue(torch.equal(result, torch.tensor([0.0, 0.0, 0.0, 0.8]))) + + +class TestUnitreeG1Retargeter(unittest.TestCase): + @patch( + "isaaclab.devices.openxr.retargeters.humanoid.unitree.inspire.g1_upper_body_retargeter.UnitreeG1DexRetargeting" + ) + def test_retarget(self, mock_dex_retargeting_cls): + mock_dex_retargeting = mock_dex_retargeting_cls.return_value + mock_dex_retargeting.get_joint_names.return_value = ["joint1", "joint2"] + mock_dex_retargeting.get_left_joint_names.return_value = ["joint1"] + mock_dex_retargeting.get_right_joint_names.return_value = ["joint2"] + mock_dex_retargeting.compute_left.return_value = np.array([0.1]) + mock_dex_retargeting.compute_right.return_value = np.array([0.2]) + + cfg = UnitreeG1RetargeterCfg( + enable_visualization=False, sim_device="cpu", hand_joint_names=["joint1", "joint2"] + ) + retargeter = UnitreeG1Retargeter(cfg) + + wrist_pose = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + data = { + DeviceBase.TrackingTarget.HAND_LEFT: {"wrist": wrist_pose}, + DeviceBase.TrackingTarget.HAND_RIGHT: {"wrist": wrist_pose}, + } + + result = retargeter.retarget(data) + self.assertEqual(result.shape, (16,)) + + +class TestGR1T2Retargeter(unittest.TestCase): + @patch("isaaclab.devices.openxr.retargeters.humanoid.fourier.gr1t2_retargeter.GR1TR2DexRetargeting") + def test_retarget(self, mock_dex_retargeting_cls): + mock_dex_retargeting = mock_dex_retargeting_cls.return_value + mock_dex_retargeting.get_joint_names.return_value = ["joint1", "joint2"] + mock_dex_retargeting.get_left_joint_names.return_value = ["joint1"] + mock_dex_retargeting.get_right_joint_names.return_value = ["joint2"] + mock_dex_retargeting.compute_left.return_value = np.array([0.1]) + mock_dex_retargeting.compute_right.return_value = np.array([0.2]) + + cfg = GR1T2RetargeterCfg(enable_visualization=False, sim_device="cpu", hand_joint_names=["joint1", "joint2"]) + retargeter = GR1T2Retargeter(cfg) + + wrist_pose = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + data = { + DeviceBase.TrackingTarget.HAND_LEFT: {"wrist": wrist_pose}, + DeviceBase.TrackingTarget.HAND_RIGHT: {"wrist": wrist_pose}, + } + + result = retargeter.retarget(data) + self.assertEqual(result.shape, (16,)) + + +class TestG1LowerBodyStandingMotionControllerRetargeter(unittest.TestCase): + def test_retarget(self): + cfg = G1LowerBodyStandingMotionControllerRetargeterCfg( + hip_height=0.8, movement_scale=1.0, rotation_scale=1.0, sim_device="cpu" + ) + retargeter = G1LowerBodyStandingMotionControllerRetargeter(cfg) + + # Mock input data + # Inputs array structure: [thumbstick_x, thumbstick_y, trigger, squeeze, button_0, button_1, padding] + left_inputs = np.zeros(7) + left_inputs[0] = 0.5 # thumbstick x + left_inputs[1] = 0.5 # thumbstick y + + right_inputs = np.zeros(7) + right_inputs[0] = -0.5 # thumbstick x + right_inputs[1] = -0.5 # thumbstick y + + data = { + DeviceBase.TrackingTarget.CONTROLLER_LEFT: [np.zeros(7), left_inputs], + DeviceBase.TrackingTarget.CONTROLLER_RIGHT: [np.zeros(7), right_inputs], + } + + result = retargeter.retarget(data) + # Output: [-left_thumbstick_y, -left_thumbstick_x, -right_thumbstick_x, hip_height] + # hip_height modified by right_thumbstick_y + + self.assertEqual(result.shape, (4,)) + self.assertAlmostEqual(result[0].item(), -0.5) # -left y + self.assertAlmostEqual(result[1].item(), -0.5) # -left x + self.assertAlmostEqual(result[2].item(), 0.5) # -right x + # Check hip height modification logic if needed, but basic execution is key here + + +class TestG1TriHandUpperBodyMotionControllerGripperRetargeter(unittest.TestCase): + def test_retarget(self): + cfg = G1TriHandUpperBodyMotionControllerGripperRetargeterCfg( + threshold_high=0.6, threshold_low=0.4, sim_device="cpu" + ) + retargeter = G1TriHandUpperBodyMotionControllerGripperRetargeter(cfg) + + pose = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + inputs_trigger_high = np.zeros(7) + inputs_trigger_high[2] = 0.8 # Trigger + + inputs_trigger_low = np.zeros(7) + inputs_trigger_low[2] = 0.2 # Trigger + + data = { + DeviceBase.TrackingTarget.CONTROLLER_LEFT: [pose, inputs_trigger_high], + DeviceBase.TrackingTarget.CONTROLLER_RIGHT: [pose, inputs_trigger_low], + } + + result = retargeter.retarget(data) + # Output: [left_state, right_state, left_wrist(7), right_wrist(7)] + self.assertEqual(result.shape, (16,)) + self.assertEqual(result[0].item(), 1.0) # Left closed + self.assertEqual(result[1].item(), 0.0) # Right open + + +class TestG1TriHandUpperBodyMotionControllerRetargeter(unittest.TestCase): + def test_retarget(self): + cfg = G1TriHandUpperBodyMotionControllerRetargeterCfg( + hand_joint_names=["dummy"] * 14, # Not really used in logic, just passed to config + sim_device="cpu", + enable_visualization=False, + ) + retargeter = G1TriHandUpperBodyMotionControllerRetargeter(cfg) + + pose = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + inputs = np.zeros(7) + + data = { + DeviceBase.TrackingTarget.CONTROLLER_LEFT: [pose, inputs], + DeviceBase.TrackingTarget.CONTROLLER_RIGHT: [pose, inputs], + } + + result = retargeter.retarget(data) + # Output: [left_wrist(7), right_wrist(7), hand_joints(14)] + self.assertEqual(result.shape, (28,)) + + +class TestG1TriHandUpperBodyRetargeter(unittest.TestCase): + @patch( + "isaaclab.devices.openxr.retargeters.humanoid.unitree.trihand.g1_upper_body_retargeter.G1TriHandDexRetargeting" + ) + def test_retarget(self, mock_dex_retargeting_cls): + mock_dex_retargeting = mock_dex_retargeting_cls.return_value + mock_dex_retargeting.get_joint_names.return_value = ["joint1", "joint2"] + mock_dex_retargeting.get_left_joint_names.return_value = ["joint1"] + mock_dex_retargeting.get_right_joint_names.return_value = ["joint2"] + mock_dex_retargeting.compute_left.return_value = np.array([0.1]) + mock_dex_retargeting.compute_right.return_value = np.array([0.2]) + + cfg = G1TriHandUpperBodyRetargeterCfg( + enable_visualization=False, sim_device="cpu", hand_joint_names=["joint1", "joint2"] + ) + retargeter = G1TriHandUpperBodyRetargeter(cfg) + + wrist_pose = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + data = { + DeviceBase.TrackingTarget.HAND_LEFT: {"wrist": wrist_pose}, + DeviceBase.TrackingTarget.HAND_RIGHT: {"wrist": wrist_pose}, + } + + result = retargeter.retarget(data) + # Output: [left_wrist(7), right_wrist(7), joints(2)] + self.assertEqual(result.shape, (16,)) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/isaaclab/test/envs/check_manager_based_env_anymal_locomotion.py b/source/isaaclab/test/envs/check_manager_based_env_anymal_locomotion.py index dfc342493340..c6169c94d197 100644 --- a/source/isaaclab/test/envs/check_manager_based_env_anymal_locomotion.py +++ b/source/isaaclab/test/envs/check_manager_based_env_anymal_locomotion.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/envs/check_manager_based_env_floating_cube.py b/source/isaaclab/test/envs/check_manager_based_env_floating_cube.py index 71447e78134a..fb7622ae67cd 100644 --- a/source/isaaclab/test/envs/check_manager_based_env_floating_cube.py +++ b/source/isaaclab/test/envs/check_manager_based_env_floating_cube.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/envs/test_action_state_recorder_term.py b/source/isaaclab/test/envs/test_action_state_recorder_term.py index 64f4a726f360..16ae866dfce2 100644 --- a/source/isaaclab/test/envs/test_action_state_recorder_term.py +++ b/source/isaaclab/test/envs/test_action_state_recorder_term.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,15 +12,16 @@ """Rest everything follows.""" -import gymnasium as gym import shutil import tempfile -import torch import uuid +import gymnasium as gym +import pytest +import torch + import carb import omni.usd -import pytest from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg diff --git a/source/isaaclab/test/envs/test_color_randomization.py b/source/isaaclab/test/envs/test_color_randomization.py index a550e7733372..619c7b3368fc 100644 --- a/source/isaaclab/test/envs/test_color_randomization.py +++ b/source/isaaclab/test/envs/test_color_randomization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,11 +18,11 @@ """Rest everything follows.""" import math + +import pytest import torch import omni.usd -import pytest -from isaacsim.core.version import get_version import isaaclab.envs.mdp as mdp from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg @@ -31,6 +31,7 @@ from isaaclab.managers import ObservationTermCfg as ObsTerm from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass +from isaaclab.utils.version import get_isaac_sim_version from isaaclab_tasks.manager_based.classic.cartpole.cartpole_env_cfg import CartpoleSceneCfg @@ -138,8 +139,7 @@ def __post_init__(self): def test_color_randomization(device): """Test color randomization for cartpole environment.""" # skip test if stage in memory is not supported - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: pytest.skip("Color randomization test hangs in this version of Isaac Sim") # Create a new stage diff --git a/source/isaaclab/test/envs/test_direct_marl_env.py b/source/isaaclab/test/envs/test_direct_marl_env.py index b9e6142b211f..d7ebd04610b4 100644 --- a/source/isaaclab/test/envs/test_direct_marl_env.py +++ b/source/isaaclab/test/envs/test_direct_marl_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +17,10 @@ """Rest everything follows.""" -import omni.usd import pytest +import omni.usd + from isaaclab.envs import DirectMARLEnv, DirectMARLEnvCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass diff --git a/source/isaaclab/test/envs/test_env_rendering_logic.py b/source/isaaclab/test/envs/test_env_rendering_logic.py index f3ba8891b9a2..70f0a01f212a 100644 --- a/source/isaaclab/test/envs/test_env_rendering_logic.py +++ b/source/isaaclab/test/envs/test_env_rendering_logic.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +13,10 @@ """Rest everything follows.""" +import pytest import torch import omni.usd -import pytest from isaaclab.envs import ( DirectRLEnv, @@ -200,9 +200,9 @@ def test_env_rendering_logic(env_type, render_interval, physics_callback, render assert num_render_steps == (i + 1) * env.cfg.decimation // env.cfg.sim.render_interval, "Render steps mismatch" # check that we have rendered for the correct amount of time render_time, _ = get_render_stats() - assert ( - abs(render_time - num_render_steps * env.cfg.sim.dt * env.cfg.sim.render_interval) < 1e-6 - ), "Render time mismatch" + assert abs(render_time - num_render_steps * env.cfg.sim.dt * env.cfg.sim.render_interval) < 1e-6, ( + "Render time mismatch" + ) # close the environment env.close() diff --git a/source/isaaclab/test/envs/test_manager_based_env.py b/source/isaaclab/test/envs/test_manager_based_env.py index c420b16f12de..7ec9ef2d43f8 100644 --- a/source/isaaclab/test/envs/test_manager_based_env.py +++ b/source/isaaclab/test/envs/test_manager_based_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 @@ """Rest everything follows.""" +import pytest import torch import omni.usd -import pytest from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg from isaaclab.managers import ObservationGroupCfg as ObsGroup diff --git a/source/isaaclab/test/envs/test_manager_based_rl_env_obs_spaces.py b/source/isaaclab/test/envs/test_manager_based_rl_env_obs_spaces.py index d8a8e8e32bee..72525ddb8e03 100644 --- a/source/isaaclab/test/envs/test_manager_based_rl_env_obs_spaces.py +++ b/source/isaaclab/test/envs/test_manager_based_rl_env_obs_spaces.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ import gymnasium as gym import numpy as np +import pytest import torch import omni.usd -import pytest from isaaclab.envs import ManagerBasedRLEnv @@ -132,9 +132,9 @@ def test_obs_space_follows_clip_contraint(env_cfg_cls, device): term_cfg = getattr(getattr(env_cfg.observations, group_name), term_name) low = -np.inf if term_cfg.clip is None else term_cfg.clip[0] high = np.inf if term_cfg.clip is None else term_cfg.clip[1] - assert isinstance( - term_space, gym.spaces.Box - ), f"Expected Box space for {term_name} in {group_name}, got {type(term_space)}" + assert isinstance(term_space, gym.spaces.Box), ( + f"Expected Box space for {term_name} in {group_name}, got {type(term_space)}" + ) assert np.all(term_space.low == low) assert np.all(term_space.high == high) diff --git a/source/isaaclab/test/envs/test_manager_based_rl_env_ui.py b/source/isaaclab/test/envs/test_manager_based_rl_env_ui.py index e3c26a86b42c..f35c11a1c401 100644 --- a/source/isaaclab/test/envs/test_manager_based_rl_env_ui.py +++ b/source/isaaclab/test/envs/test_manager_based_rl_env_ui.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/envs/test_modify_env_param_curr_term.py b/source/isaaclab/test/envs/test_modify_env_param_curr_term.py index e82a842ec950..a23a29f38606 100644 --- a/source/isaaclab/test/envs/test_modify_env_param_curr_term.py +++ b/source/isaaclab/test/envs/test_modify_env_param_curr_term.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +10,10 @@ # launch omniverse app simulation_app = AppLauncher(headless=True).app +import pytest import torch import omni.usd -import pytest import isaaclab.envs.mdp as mdp from isaaclab.assets import Articulation diff --git a/source/isaaclab/test/envs/test_null_command_term.py b/source/isaaclab/test/envs/test_null_command_term.py index f8699439477a..c394fc94d5ce 100644 --- a/source/isaaclab/test/envs/test_null_command_term.py +++ b/source/isaaclab/test/envs/test_null_command_term.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/envs/test_scale_randomization.py b/source/isaaclab/test/envs/test_scale_randomization.py index 82c2127bc6e3..282c6b2a3d85 100644 --- a/source/isaaclab/test/envs/test_scale_randomization.py +++ b/source/isaaclab/test/envs/test_scale_randomization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -20,21 +20,20 @@ """Rest everything follows.""" +import pytest import torch import omni.usd -import pytest from pxr import Sdf import isaaclab.envs.mdp as mdp 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 diff --git a/source/isaaclab/test/envs/test_spaces_utils.py b/source/isaaclab/test/envs/test_spaces_utils.py index cbb6fc0e2db8..f170173ea384 100644 --- a/source/isaaclab/test/envs/test_spaces_utils.py +++ b/source/isaaclab/test/envs/test_spaces_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/envs/test_texture_randomization.py b/source/isaaclab/test/envs/test_texture_randomization.py index 417825423acc..e2cbe7d54486 100644 --- a/source/isaaclab/test/envs/test_texture_randomization.py +++ b/source/isaaclab/test/envs/test_texture_randomization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,10 +18,11 @@ """Rest everything follows.""" import math + +import pytest import torch import omni.usd -import pytest import isaaclab.envs.mdp as mdp from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg diff --git a/source/isaaclab/test/managers/test_event_manager.py b/source/isaaclab/test/managers/test_event_manager.py index 30f2e42699d4..171cc8be65e9 100644 --- a/source/isaaclab/test/managers/test_event_manager.py +++ b/source/isaaclab/test/managers/test_event_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +7,7 @@ # pyright: reportPrivateUsage=none """Launch Isaac Sim Simulator first.""" + from collections.abc import Sequence from isaaclab.app import AppLauncher @@ -17,10 +18,10 @@ """Rest everything follows.""" -import torch from collections import namedtuple import pytest +import torch from isaaclab.envs import ManagerBasedEnv from isaaclab.managers import EventManager, EventTermCfg, ManagerTermBase, ManagerTermBaseCfg diff --git a/source/isaaclab/test/managers/test_observation_manager.py b/source/isaaclab/test/managers/test_observation_manager.py index 7346a3199f0b..d738f179da71 100644 --- a/source/isaaclab/test/managers/test_observation_manager.py +++ b/source/isaaclab/test/managers/test_observation_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +15,11 @@ """Rest everything follows.""" -import torch from collections import namedtuple from typing import TYPE_CHECKING import pytest +import torch import isaaclab.sim as sim_utils from isaaclab.managers import ( @@ -88,7 +88,6 @@ def call_me(self, env: object) -> torch.Tensor: class MyDataClass: - def __init__(self, num_envs: int, device: str): self.pos_w = torch.rand((num_envs, 3), device=device) self.lin_vel_w = torch.rand((num_envs, 3), device=device) @@ -283,7 +282,6 @@ class SampleMixedGroupCfg(ObservationGroupCfg): @configclass class SampleImageGroupCfg(ObservationGroupCfg): - term_1 = ObservationTermCfg(func=grilled_chicken_image, scale=1.5, params={"bland": 0.5, "channel": 1}) term_2 = ObservationTermCfg(func=grilled_chicken_image, scale=0.5, params={"bland": 0.1, "channel": 3}) @@ -337,7 +335,6 @@ class CriticCfg(ObservationGroupCfg): @configclass class ImageCfg(ObservationGroupCfg): - term_1 = ObservationTermCfg(func=grilled_chicken_image, scale=1.5, params={"bland": 0.5, "channel": 1}) term_2 = ObservationTermCfg(func=grilled_chicken_image, scale=0.5, params={"bland": 0.1, "channel": 3}) @@ -675,7 +672,6 @@ def test_serialize(setup_env): serialize_data = {"test": 0} class test_serialize_term(ManagerTermBase): - def __init__(self, cfg: RewardTermCfg, env: ManagerBasedEnv): super().__init__(cfg, env) diff --git a/source/isaaclab/test/managers/test_recorder_manager.py b/source/isaaclab/test/managers/test_recorder_manager.py index e36e33122f01..8a8e8c78a9d2 100644 --- a/source/isaaclab/test/managers/test_recorder_manager.py +++ b/source/isaaclab/test/managers/test_recorder_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,18 +17,26 @@ import os import shutil import tempfile -import torch import uuid from collections import namedtuple from collections.abc import Sequence +from typing import TYPE_CHECKING +import h5py import pytest +import torch + +import omni.usd -from isaaclab.envs import ManagerBasedEnv +from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg from isaaclab.managers import DatasetExportMode, RecorderManager, RecorderManagerBaseCfg, RecorderTerm, RecorderTermCfg +from isaaclab.scene import InteractiveSceneCfg from isaaclab.sim import SimulationContext from isaaclab.utils import configclass +if TYPE_CHECKING: + import numpy as np + class DummyResetRecorderTerm(RecorderTerm): """Dummy recorder term that records dummy data.""" @@ -78,6 +86,72 @@ class DummyStepRecorderTermCfg(RecorderTermCfg): dataset_export_mode = DatasetExportMode.EXPORT_ALL +@configclass +class EmptyManagerCfg: + """Empty manager specifications for the environment.""" + + pass + + +@configclass +class EmptySceneCfg(InteractiveSceneCfg): + """Configuration for an empty scene.""" + + pass + + +def get_empty_base_env_cfg(device: str = "cuda", num_envs: int = 1, env_spacing: float = 1.0): + """Generate base environment config based on device""" + + @configclass + class EmptyEnvCfg(ManagerBasedEnvCfg): + """Configuration for the empty test environment.""" + + # Scene settings + scene: EmptySceneCfg = EmptySceneCfg(num_envs=num_envs, env_spacing=env_spacing) + # Basic settings + actions: EmptyManagerCfg = EmptyManagerCfg() + observations: EmptyManagerCfg = EmptyManagerCfg() + recorders: EmptyManagerCfg = EmptyManagerCfg() + + def __post_init__(self): + """Post initialization.""" + # step settings + self.decimation = 4 # env step every 4 sim steps: 200Hz / 4 = 50Hz + # simulation settings + self.sim.dt = 0.005 # sim step every 5ms: 200Hz + self.sim.render_interval = self.decimation # render every 4 sim steps + # pass device down from test + self.sim.device = device + + return EmptyEnvCfg() + + +def get_file_contents(file_name: str, num_steps: int) -> dict[str, np.ndarray]: + """Retrieves the contents of the hdf5 file + Args: + file_name: absolute path to the hdf5 file + num_steps: number of steps taken in the environment + Returns: + dict[str, np.ndarray]: dictionary where keys are HDF5 paths and values are the corresponding data arrays. + """ + data = {} + with h5py.File(file_name, "r") as f: + + def get_data(name, obj): + if isinstance(obj, h5py.Dataset): + if "record_post_step" in name: + assert obj[()].shape == (num_steps, 5) + elif "record_pre_step" in name: + assert obj[()].shape == (num_steps, 4) + else: + raise Exception(f"The hdf5 file contains an unexpected data path, {name}") + data[name] = obj[()] + + f.visititems(get_data) + return data + + @configclass class DummyEnvCfg: """Dummy environment configuration.""" @@ -146,36 +220,63 @@ def test_initialize_dataset_file(dataset_dir): assert os.path.exists(os.path.join(cfg.dataset_export_dir_path, cfg.dataset_filename)) -def test_record(dataset_dir): +@pytest.mark.parametrize("device", ("cpu", "cuda")) +def test_record(device, dataset_dir): """Test the recording of the data.""" - for device in ("cuda:0", "cpu"): - env = create_dummy_env(device) - # create recorder manager - cfg = DummyRecorderManagerCfg() - cfg.dataset_export_dir_path = dataset_dir - cfg.dataset_filename = f"{uuid.uuid4()}.hdf5" - recorder_manager = RecorderManager(cfg, env) - - # record the step data - recorder_manager.record_pre_step() - recorder_manager.record_post_step() - - recorder_manager.record_pre_step() - recorder_manager.record_post_step() - - # check the recorded data - for env_id in range(env.num_envs): - episode = recorder_manager.get_episode(env_id) - assert torch.stack(episode.data["record_pre_step"]).shape == (2, 4) - assert torch.stack(episode.data["record_post_step"]).shape == (2, 5) - - # Trigger pre-reset callbacks which then export and clean the episode data - recorder_manager.record_pre_reset(env_ids=None) - for env_id in range(env.num_envs): - episode = recorder_manager.get_episode(env_id) - assert episode.is_empty() - - recorder_manager.record_post_reset(env_ids=None) - for env_id in range(env.num_envs): - episode = recorder_manager.get_episode(env_id) - assert torch.stack(episode.data["record_post_reset"]).shape == (1, 3) + env = create_dummy_env(device) + # create recorder manager + cfg = DummyRecorderManagerCfg() + cfg.dataset_export_dir_path = dataset_dir + cfg.dataset_filename = f"{uuid.uuid4()}.hdf5" + recorder_manager = RecorderManager(cfg, env) + + # record the step data + recorder_manager.record_pre_step() + recorder_manager.record_post_step() + + recorder_manager.record_pre_step() + recorder_manager.record_post_step() + + # check the recorded data + for env_id in range(env.num_envs): + episode = recorder_manager.get_episode(env_id) + assert torch.stack(episode.data["record_pre_step"]).shape == (2, 4) + assert torch.stack(episode.data["record_post_step"]).shape == (2, 5) + + # Trigger pre-reset callbacks which then export and clean the episode data + recorder_manager.record_pre_reset(env_ids=None) + for env_id in range(env.num_envs): + episode = recorder_manager.get_episode(env_id) + assert episode.is_empty() + + recorder_manager.record_post_reset(env_ids=None) + for env_id in range(env.num_envs): + episode = recorder_manager.get_episode(env_id) + assert torch.stack(episode.data["record_post_reset"]).shape == (1, 3) + + +@pytest.mark.parametrize("device", ("cpu", "cuda")) +def test_close(device, dataset_dir): + """Test whether data is correctly exported in the close function when fully integrated with ManagerBasedEnv and + `export_in_close` is True.""" + # create a new stage + omni.usd.get_context().new_stage() + # create environment + env_cfg = get_empty_base_env_cfg(device=device, num_envs=2) + cfg = DummyRecorderManagerCfg() + cfg.export_in_close = True + cfg.dataset_export_dir_path = dataset_dir + cfg.dataset_filename = f"{uuid.uuid4()}.hdf5" + env_cfg.recorders = cfg + env = ManagerBasedEnv(cfg=env_cfg) + num_steps = 3 + for _ in range(num_steps): + act = torch.randn_like(env.action_manager.action) + obs, ext = env.step(act) + # check contents of hdf5 file + file_name = f"{env_cfg.recorders.dataset_export_dir_path}/{env_cfg.recorders.dataset_filename}" + data_pre_close = get_file_contents(file_name, num_steps) + assert len(data_pre_close) == 0 + env.close() + data_post_close = get_file_contents(file_name, num_steps) + assert len(data_post_close.keys()) == 2 * env_cfg.scene.num_envs diff --git a/source/isaaclab/test/managers/test_reward_manager.py b/source/isaaclab/test/managers/test_reward_manager.py index 1b023d74ea7d..8301fac5b504 100644 --- a/source/isaaclab/test/managers/test_reward_manager.py +++ b/source/isaaclab/test/managers/test_reward_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ """Rest everything follows.""" -import torch from collections import namedtuple import pytest +import torch from isaaclab.managers import RewardManager, RewardTermCfg from isaaclab.sim import SimulationContext diff --git a/source/isaaclab/test/managers/test_termination_manager.py b/source/isaaclab/test/managers/test_termination_manager.py new file mode 100644 index 000000000000..db96e93675c3 --- /dev/null +++ b/source/isaaclab/test/managers/test_termination_manager.py @@ -0,0 +1,139 @@ +# 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 + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import pytest +import torch + +from isaaclab.managers import TerminationManager, TerminationTermCfg +from isaaclab.sim import SimulationContext + + +class DummyEnv: + """Minimal mutable env stub for the termination manager tests.""" + + def __init__(self, num_envs: int, device: str, sim: SimulationContext): + self.num_envs = num_envs + self.device = device + self.sim = sim + self.counter = 0 # mutable step counter used by test terms + + +def fail_every_5_steps(env) -> torch.Tensor: + """Returns True for all envs when counter is a positive multiple of 5.""" + cond = env.counter > 0 and (env.counter % 5 == 0) + return torch.full((env.num_envs,), cond, dtype=torch.bool, device=env.device) + + +def fail_every_10_steps(env) -> torch.Tensor: + """Returns True for all envs when counter is a positive multiple of 10.""" + cond = env.counter > 0 and (env.counter % 10 == 0) + return torch.full((env.num_envs,), cond, dtype=torch.bool, device=env.device) + + +def fail_every_3_steps(env) -> torch.Tensor: + """Returns True for all envs when counter is a positive multiple of 3.""" + cond = env.counter > 0 and (env.counter % 3 == 0) + return torch.full((env.num_envs,), cond, dtype=torch.bool, device=env.device) + + +@pytest.fixture +def env(): + sim = SimulationContext() + return DummyEnv(num_envs=20, device="cpu", sim=sim) + + +def test_initial_state_and_shapes(env): + cfg = { + "term_5": TerminationTermCfg(func=fail_every_5_steps), + "term_10": TerminationTermCfg(func=fail_every_10_steps), + } + tm = TerminationManager(cfg, env) + + # Active term names + assert tm.active_terms == ["term_5", "term_10"] + + # Internal buffers have expected shapes and start as all False + assert tm._term_dones.shape == (env.num_envs, 2) + assert tm._last_episode_dones.shape == (env.num_envs, 2) + assert tm.dones.shape == (env.num_envs,) + assert tm.time_outs.shape == (env.num_envs,) + assert tm.terminated.shape == (env.num_envs,) + assert torch.all(~tm._term_dones) and torch.all(~tm._last_episode_dones) + + +def test_term_transitions_and_persistence(env): + """Concise transitions: single fire, persist, switch, both, persist. + + Uses 3-step and 5-step terms and verifies current-step values and last-episode persistence. + """ + cfg = { + "term_3": TerminationTermCfg(func=fail_every_3_steps, time_out=False), + "term_5": TerminationTermCfg(func=fail_every_5_steps, time_out=False), + } + tm = TerminationManager(cfg, env) + + # step 3: only term_3 -> last_episode [True, False] + env.counter = 3 + out = tm.compute() + assert torch.all(tm.get_term("term_3")) and torch.all(~tm.get_term("term_5")) + assert torch.all(out) + assert torch.all(tm._last_episode_dones[:, 0]) and torch.all(~tm._last_episode_dones[:, 1]) + + # step 4: none -> last_episode persists [True, False] + env.counter = 4 + out = tm.compute() + assert torch.all(~out) + assert torch.all(~tm.get_term("term_3")) and torch.all(~tm.get_term("term_5")) + assert torch.all(tm._last_episode_dones[:, 0]) and torch.all(~tm._last_episode_dones[:, 1]) + + # step 5: only term_5 -> last_episode [False, True] + env.counter = 5 + out = tm.compute() + assert torch.all(~tm.get_term("term_3")) and torch.all(tm.get_term("term_5")) + assert torch.all(out) + assert torch.all(~tm._last_episode_dones[:, 0]) and torch.all(tm._last_episode_dones[:, 1]) + + # step 15: both -> last_episode [True, True] + env.counter = 15 + out = tm.compute() + assert torch.all(tm.get_term("term_3")) and torch.all(tm.get_term("term_5")) + assert torch.all(out) + assert torch.all(tm._last_episode_dones[:, 0]) and torch.all(tm._last_episode_dones[:, 1]) + + # step 16: none -> persist [True, True] + env.counter = 16 + out = tm.compute() + assert torch.all(~out) + assert torch.all(~tm.get_term("term_3")) and torch.all(~tm.get_term("term_5")) + assert torch.all(tm._last_episode_dones[:, 0]) and torch.all(tm._last_episode_dones[:, 1]) + + +def test_time_out_vs_terminated_split(env): + cfg = { + "term_5": TerminationTermCfg(func=fail_every_5_steps, time_out=False), # terminated + "term_10": TerminationTermCfg(func=fail_every_10_steps, time_out=True), # timeout + } + tm = TerminationManager(cfg, env) + + # Step 5: terminated fires, not timeout + env.counter = 5 + out = tm.compute() + assert torch.all(out) + assert torch.all(tm.terminated) and torch.all(~tm.time_outs) + + # Step 10: both fire; timeout and terminated both True + env.counter = 10 + out = tm.compute() + assert torch.all(out) + assert torch.all(tm.terminated) and torch.all(tm.time_outs) diff --git a/source/isaaclab/test/markers/check_markers_visibility.py b/source/isaaclab/test/markers/check_markers_visibility.py index 24f38300f388..98dbee8ddcd7 100644 --- a/source/isaaclab/test/markers/check_markers_visibility.py +++ b/source/isaaclab/test/markers/check_markers_visibility.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/markers/test_visualization_markers.py b/source/isaaclab/test/markers/test_visualization_markers.py index 03955076b6ef..ebc183b804b8 100644 --- a/source/isaaclab/test/markers/test_visualization_markers.py +++ b/source/isaaclab/test/markers/test_visualization_markers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,15 +12,13 @@ """Rest everything follows.""" -import torch - -import isaacsim.core.utils.stage as stage_utils import pytest -from isaacsim.core.api.simulation_context import SimulationContext +import torch import isaaclab.sim as sim_utils from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg from isaaclab.markers.config import FRAME_MARKER_CFG, POSITION_GOAL_MARKER_CFG +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils.math import random_orientation from isaaclab.utils.timer import Timer @@ -31,14 +29,15 @@ def sim(): # Simulation time-step dt = 0.01 # Open a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Load kit helper - sim_context = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="torch", device="cuda:0") + sim_context = SimulationContext(SimulationCfg(dt=dt)) yield sim_context # Cleanup + sim_context._disable_app_control_on_stop_handle = True # prevent timeout sim_context.stop() sim_context.clear_instance() - stage_utils.close_stage() + sim_utils.close_stage() def test_instantiation(sim): diff --git a/source/isaaclab/test/performance/test_kit_startup_performance.py b/source/isaaclab/test/performance/test_kit_startup_performance.py index dfa716cd0b23..f4134d04ae14 100644 --- a/source/isaaclab/test/performance/test_kit_startup_performance.py +++ b/source/isaaclab/test/performance/test_kit_startup_performance.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/performance/test_robot_load_performance.py b/source/isaaclab/test/performance/test_robot_load_performance.py index bca8c36d9d5d..42d5f1c4fffb 100644 --- a/source/isaaclab/test/performance/test_robot_load_performance.py +++ b/source/isaaclab/test/performance/test_robot_load_performance.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,16 +13,17 @@ # launch omniverse app simulation_app = AppLauncher(headless=True).app -import omni import pytest -from isaacsim.core.cloner import GridCloner -from isaaclab_assets import ANYMAL_D_CFG, CARTPOLE_CFG +import omni +from isaacsim.core.cloner import GridCloner from isaaclab.assets import Articulation from isaaclab.sim import build_simulation_context from isaaclab.utils.timer import Timer +from isaaclab_assets import ANYMAL_D_CFG, CARTPOLE_CFG + @pytest.mark.parametrize( "test_config,device", diff --git a/source/isaaclab/test/scene/check_interactive_scene.py b/source/isaaclab/test/scene/check_interactive_scene.py index fb9b59760d06..5b2463b315a9 100644 --- a/source/isaaclab/test/scene/check_interactive_scene.py +++ b/source/isaaclab/test/scene/check_interactive_scene.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/scene/test_interactive_scene.py b/source/isaaclab/test/scene/test_interactive_scene.py index f900c7ee44ac..1a42a340baa1 100644 --- a/source/isaaclab/test/scene/test_interactive_scene.py +++ b/source/isaaclab/test/scene/test_interactive_scene.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +12,8 @@ """Rest everything follows.""" -import torch - import pytest +import torch import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg diff --git a/source/isaaclab/test/sensors/check_contact_sensor.py b/source/isaaclab/test/sensors/check_contact_sensor.py index 30d2c9be4374..b4fe5f555dc7 100644 --- a/source/isaaclab/test/sensors/check_contact_sensor.py +++ b/source/isaaclab/test/sensors/check_contact_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -36,15 +36,12 @@ import torch -import isaacsim.core.utils.prims as prim_utils -from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.cloner import GridCloner -from isaacsim.core.utils.carb import set_carb_setting -from isaacsim.core.utils.viewports import set_camera_view import isaaclab.sim as sim_utils from isaaclab.assets import Articulation from isaaclab.sensors.contact_sensor import ContactSensor, ContactSensorCfg +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils.timer import Timer ## @@ -77,19 +74,19 @@ def main(): """Spawns the ANYmal robot and clones it using Isaac Sim Cloner API.""" # Load kit helper - sim = SimulationContext(physics_dt=0.005, rendering_dt=0.005, backend="torch", device="cuda:0") + sim = SimulationContext(SimulationCfg(dt=0.005)) # Set main camera - set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0]) + sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0]) # Enable hydra scene-graph instancing # this is needed to visualize the scene when flatcache is enabled - set_carb_setting(sim._settings, "/persistent/omnihydra/useSceneGraphInstancing", True) + sim._settings.set_bool("/persistent/omnihydra/useSceneGraphInstancing", True) # Create interface to clone the scene cloner = GridCloner(spacing=2.0) cloner.define_base_env("/World/envs") # Everything under the namespace "/World/envs/env_0" will be cloned - prim_utils.define_prim("/World/envs/env_0") + sim.stage.DefinePrim("/World/envs/env_0", "Xform") # Clone the scene num_envs = args_cli.num_robots cloner.define_base_env("/World/envs") @@ -106,6 +103,7 @@ def main(): prim_path="/World/envs/env_.*/Robot/.*_FOOT", track_air_time=True, track_contact_points=True, + track_friction_forces=True, debug_vis=False, # not args_cli.headless, filter_prim_paths_expr=["/World/defaultGroundPlane/GroundPlane/CollisionPlane"], ) diff --git a/source/isaaclab/test/sensors/check_imu_sensor.py b/source/isaaclab/test/sensors/check_imu_sensor.py index 652e2d950733..8a8c048ed62d 100644 --- a/source/isaaclab/test/sensors/check_imu_sensor.py +++ b/source/isaaclab/test/sensors/check_imu_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,12 +35,12 @@ """Rest everything follows.""" -import torch +import logging import traceback -import carb +import torch + import omni -from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.cloner import GridCloner from isaacsim.core.utils.viewports import set_camera_view from pxr import PhysxSchema @@ -49,11 +49,15 @@ import isaaclab.terrains as terrain_gen from isaaclab.assets import RigidObject, RigidObjectCfg from isaaclab.sensors.imu import Imu, ImuCfg +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG from isaaclab.terrains.terrain_importer import TerrainImporter from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR from isaaclab.utils.timer import Timer +# import logger +logger = logging.getLogger(__name__) + def design_scene(sim: SimulationContext, num_envs: int = 2048) -> RigidObject: """Design the scene.""" @@ -100,7 +104,7 @@ def design_scene(sim: SimulationContext, num_envs: int = 2048) -> RigidObject: for prim in stage.Traverse(): if prim.HasAPI(PhysxSchema.PhysxSceneAPI): physics_scene_prim_path = prim.GetPrimPath() - carb.log_info(f"Physics scene prim path: {physics_scene_prim_path}") + logging.info(f"Physics scene prim path: {physics_scene_prim_path}") break # filter collisions within each environment instance cloner.filter_collisions( @@ -115,16 +119,7 @@ def main(): """Main function.""" # Load kit helper - sim_params = { - "use_gpu": True, - "use_gpu_pipeline": True, - "use_flatcache": True, # deprecated from Isaac Sim 2023.1 onwards - "use_fabric": True, # used from Isaac Sim 2023.1 onwards - "enable_scene_query_support": True, - } - sim = SimulationContext( - physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, sim_params=sim_params, backend="torch", device="cuda:0" - ) + sim = SimulationContext(SimulationCfg()) # Set main camera set_camera_view([0.0, 30.0, 25.0], [0.0, 0.0, -2.5]) @@ -188,8 +183,8 @@ def main(): # Run the main function main() except Exception as err: - carb.log_error(err) - carb.log_error(traceback.format_exc()) + logger.error(err) + logger.error(traceback.format_exc()) raise finally: # close sim app diff --git a/source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py b/source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py new file mode 100644 index 000000000000..73750d0de874 --- /dev/null +++ b/source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py @@ -0,0 +1,206 @@ +# 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 shows how to use the multi-mesh ray caster from the Isaac Lab framework. + +.. code-block:: bash + + # Usage + ./isaaclab.sh -p source/isaaclab/test/sensors/check_multi_mesh_ray_caster.py --headless + +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Ray Caster Test Script") +parser.add_argument("--num_envs", type=int, default=16, help="Number of environments to clone.") +parser.add_argument("--num_objects", type=int, default=0, help="Number of additional objects to clone.") +parser.add_argument( + "--terrain_type", + type=str, + default="generator", + help="Type of terrain to import. Can be 'generator' or 'usd' or 'plane'.", +) +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + + +"""Rest everything follows.""" + +import random + +import torch + +from isaacsim.core.cloner import GridCloner + +import isaaclab.sim as sim_utils +import isaaclab.terrains as terrain_gen +from isaaclab.assets import RigidObject, RigidObjectCfg +from isaaclab.sensors.ray_caster import MultiMeshRayCaster, MultiMeshRayCasterCfg, patterns +from isaaclab.sim import SimulationCfg, SimulationContext +from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG +from isaaclab.terrains.terrain_importer import TerrainImporter +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.math import quat_from_euler_xyz +from isaaclab.utils.timer import Timer + + +def design_scene(sim: SimulationContext, num_envs: int = 2048): + """Design the scene.""" + # Create interface to clone the scene + cloner = GridCloner(spacing=10.0) + cloner.define_base_env("/World/envs") + # Everything under the namespace "/World/envs/env_0" will be cloned + sim.stage.DefinePrim("/World/envs/env_0", "Xform") + # Define the scene + # -- Light + cfg = sim_utils.DistantLightCfg(intensity=2000) + cfg.func("/World/light", cfg) + # -- Balls + cfg = sim_utils.SphereCfg( + radius=0.25, + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0)), + ) + cfg.func("/World/envs/env_0/ball", cfg, translation=(0.0, 0.0, 5.0)) + + for i in range(args_cli.num_objects): + object = sim_utils.CuboidCfg( + size=(0.5 + random.random() * 0.5, 0.5 + random.random() * 0.5, 0.1 + random.random() * 0.05), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg( + diffuse_color=(0.0 + i / args_cli.num_objects, 0.0, 1.0 - i / args_cli.num_objects) + ), + ) + object.func( + f"/World/envs/env_0/object_{i}", + object, + translation=(0.0 + random.random(), 0.0 + random.random(), 1.0), + orientation=quat_from_euler_xyz(torch.Tensor(0), torch.Tensor(0), torch.rand(1) * torch.pi).numpy(), + ) + + # Clone the scene + cloner.define_base_env("/World/envs") + envs_prim_paths = cloner.generate_paths("/World/envs/env", num_paths=num_envs) + cloner.clone(source_prim_path="/World/envs/env_0", prim_paths=envs_prim_paths, replicate_physics=True) + physics_scene_path = sim.get_physics_context().prim_path + cloner.filter_collisions( + physics_scene_path, "/World/collisions", prim_paths=envs_prim_paths, global_paths=["/World/ground"] + ) + + +def main(): + """Main function.""" + # Load kit helper + sim = SimulationContext(SimulationCfg()) + # Set main camera + sim.set_camera_view([0.0, 30.0, 25.0], [0.0, 0.0, -2.5]) + + # Parameters + num_envs = args_cli.num_envs + # Design the scene + design_scene(sim=sim, num_envs=num_envs) + # Handler for terrains importing + terrain_importer_cfg = terrain_gen.TerrainImporterCfg( + prim_path="/World/ground", + terrain_type=args_cli.terrain_type, + terrain_generator=ROUGH_TERRAINS_CFG, + usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd", + max_init_terrain_level=0, + num_envs=1, + ) + _ = TerrainImporter(terrain_importer_cfg) + + mesh_targets: list[MultiMeshRayCasterCfg.RaycastTargetCfg] = [ + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="/World/ground", track_mesh_transforms=False), + ] + if args_cli.num_objects != 0: + mesh_targets.append( + MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="/World/envs/env_.*/object_.*", track_mesh_transforms=True) + ) + # Create a ray-caster sensor + ray_caster_cfg = MultiMeshRayCasterCfg( + prim_path="/World/envs/env_.*/ball", + mesh_prim_paths=mesh_targets, + pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=(1.6, 1.0)), + attach_yaw_only=True, + debug_vis=not args_cli.headless, + ) + ray_caster = MultiMeshRayCaster(cfg=ray_caster_cfg) + # Create a view over all the balls + balls_cfg = RigidObjectCfg( + prim_path="/World/envs/env_.*/ball", + spawn=None, + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 5.0)), + ) + balls = RigidObject(cfg=balls_cfg) + + # Play simulator + sim.reset() + + # Initialize the views + # -- balls + print(balls) + # Print the sensor information + print(ray_caster) + + # Get the initial positions of the balls + ball_initial_poses = balls.data.root_pose_w.clone() + ball_initial_velocities = balls.data.root_vel_w.clone() + + # Create a counter for resetting the scene + step_count = 0 + # Simulate physics + while simulation_app.is_running(): + # If simulation is stopped, then exit. + if sim.is_stopped(): + break + # If simulation is paused, then skip. + if not sim.is_playing(): + sim.step(render=False) + continue + # Reset the scene + if step_count % 500 == 0: + # sample random indices to reset + reset_indices = torch.randint(0, num_envs, (num_envs // 2,), device=sim.device) + # reset the balls + balls.write_root_pose_to_sim(ball_initial_poses[reset_indices], env_ids=reset_indices) + balls.write_root_velocity_to_sim(ball_initial_velocities[reset_indices], env_ids=reset_indices) + balls.reset(reset_indices) + # reset the sensor + ray_caster.reset(reset_indices) + # reset the counter + step_count = 0 + # Step simulation + sim.step() + # Update the ray-caster + with Timer(f"Ray-caster update with {num_envs} x {ray_caster.num_rays} rays"): + ray_caster.update(dt=sim.get_physics_dt(), force_recompute=True) + # Update counter + step_count += 1 + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/source/isaaclab/test/sensors/check_ray_caster.py b/source/isaaclab/test/sensors/check_ray_caster.py index e1d3473ecc47..78f314fdebd6 100644 --- a/source/isaaclab/test/sensors/check_ray_caster.py +++ b/source/isaaclab/test/sensors/check_ray_caster.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -41,15 +41,13 @@ import torch -import isaacsim.core.utils.prims as prim_utils -from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.cloner import GridCloner -from isaacsim.core.prims import RigidPrim -from isaacsim.core.utils.viewports import set_camera_view import isaaclab.sim as sim_utils import isaaclab.terrains as terrain_gen +from isaaclab.assets import RigidObject, RigidObjectCfg from isaaclab.sensors.ray_caster import RayCaster, RayCasterCfg, patterns +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG from isaaclab.terrains.terrain_importer import TerrainImporter from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -62,7 +60,7 @@ def design_scene(sim: SimulationContext, num_envs: int = 2048): cloner = GridCloner(spacing=2.0) cloner.define_base_env("/World/envs") # Everything under the namespace "/World/envs/env_0" will be cloned - prim_utils.define_prim("/World/envs/env_0") + sim.stage.DefinePrim("/World/envs/env_0", "Xform") # Define the scene # -- Light cfg = sim_utils.DistantLightCfg(intensity=2000) @@ -89,19 +87,9 @@ def design_scene(sim: SimulationContext, num_envs: int = 2048): def main(): """Main function.""" - # Load kit helper - sim_params = { - "use_gpu": True, - "use_gpu_pipeline": True, - "use_flatcache": True, # deprecated from Isaac Sim 2023.1 onwards - "use_fabric": True, # used from Isaac Sim 2023.1 onwards - "enable_scene_query_support": True, - } - sim = SimulationContext( - physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, sim_params=sim_params, backend="torch", device="cuda:0" - ) + sim = SimulationContext(SimulationCfg()) # Set main camera - set_camera_view([0.0, 30.0, 25.0], [0.0, 0.0, -2.5]) + sim.set_camera_view([0.0, 30.0, 25.0], [0.0, 0.0, -2.5]) # Parameters num_envs = args_cli.num_envs @@ -128,20 +116,25 @@ def main(): ) ray_caster = RayCaster(cfg=ray_caster_cfg) # Create a view over all the balls - ball_view = RigidPrim("/World/envs/env_.*/ball", reset_xform_properties=False) + balls_cfg = RigidObjectCfg( + prim_path="/World/envs/env_.*/ball", + spawn=None, + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 5.0)), + ) + balls = RigidObject(cfg=balls_cfg) # Play simulator sim.reset() # Initialize the views # -- balls - ball_view.initialize() + print(balls) # Print the sensor information print(ray_caster) # Get the initial positions of the balls - ball_initial_positions, ball_initial_orientations = ball_view.get_world_poses() - ball_initial_velocities = ball_view.get_velocities() + ball_initial_poses = balls.data.root_pose_w.clone() + ball_initial_velocities = balls.data.root_vel_w.clone() # Create a counter for resetting the scene step_count = 0 @@ -157,12 +150,11 @@ def main(): # Reset the scene if step_count % 500 == 0: # sample random indices to reset - reset_indices = torch.randint(0, num_envs, (num_envs // 2,)) + reset_indices = torch.randint(0, num_envs, (num_envs // 2,), device=sim.device) # reset the balls - ball_view.set_world_poses( - ball_initial_positions[reset_indices], ball_initial_orientations[reset_indices], indices=reset_indices - ) - ball_view.set_velocities(ball_initial_velocities[reset_indices], indices=reset_indices) + balls.write_root_pose_to_sim(ball_initial_poses[reset_indices], env_ids=reset_indices) + balls.write_root_velocity_to_sim(ball_initial_velocities[reset_indices], env_ids=reset_indices) + balls.reset(reset_indices) # reset the sensor ray_caster.reset(reset_indices) # reset the counter diff --git a/source/isaaclab/test/sensors/test_camera.py b/source/isaaclab/test/sensors/test_camera.py index e660274b862a..584394bfd54f 100644 --- a/source/isaaclab/test/sensors/test_camera.py +++ b/source/isaaclab/test/sensors/test_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,16 +16,15 @@ """Rest everything follows.""" import copy -import numpy as np import os import random + +import numpy as np +import pytest import scipy.spatial.transform as tf import torch -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import omni.replicator.core as rep -import pytest from isaacsim.core.prims import SingleGeometryPrim, SingleRigidPrim from pxr import Gf, Usd, UsdGeom @@ -60,7 +59,7 @@ def setup() -> tuple[sim_utils.SimulationContext, CameraCfg, float]: ), ) # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.01 # Load kit helper @@ -69,7 +68,7 @@ def setup() -> tuple[sim_utils.SimulationContext, CameraCfg, float]: # populate scene _populate_scene() # load stage - stage_utils.update_stage() + sim_utils.update_stage() return sim, camera_cfg, dt @@ -891,7 +890,7 @@ def _populate_scene(): position *= np.asarray([1.5, 1.5, 0.5]) # create prim prim_type = random.choice(["Cube", "Sphere", "Cylinder"]) - prim = prim_utils.create_prim( + prim = sim_utils.create_prim( f"/World/Objects/Obj_{i:02d}", prim_type, translation=position, diff --git a/source/isaaclab/test/sensors/test_contact_sensor.py b/source/isaaclab/test/sensors/test_contact_sensor.py index 4512b29f3b20..ed376f97f2d1 100644 --- a/source/isaaclab/test/sensors/test_contact_sensor.py +++ b/source/isaaclab/test/sensors/test_contact_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,19 +14,22 @@ """Rest everything follows.""" -import torch from dataclasses import MISSING from enum import Enum -import carb import pytest +import torch from flaky import flaky +import carb +from pxr import PhysxSchema + import isaaclab.sim as sim_utils from isaaclab.assets import RigidObject, RigidObjectCfg from isaaclab.scene import InteractiveScene, InteractiveSceneCfg from isaaclab.sensors import ContactSensor, ContactSensorCfg -from isaaclab.sim import SimulationContext, build_simulation_context +from isaaclab.sim import SimulationCfg, SimulationContext, build_simulation_context +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.terrains import HfRandomUniformTerrainCfg, TerrainGeneratorCfg, TerrainImporterCfg from isaaclab.utils import configclass @@ -395,6 +398,178 @@ def test_sensor_print(setup_simulation): print(scene.sensors["contact_sensor"]) +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_contact_sensor_threshold(setup_simulation, device): + """Test that the contact sensor USD threshold attribute is set to 0.0.""" + sim_dt, durations, terrains, devices, carb_settings_iface = setup_simulation + with build_simulation_context(device=device, dt=sim_dt, add_lighting=False) as sim: + sim._app_control_on_stop_handle = None + # Spawn things into stage + scene_cfg = ContactSensorSceneCfg(num_envs=1, env_spacing=1.0, lazy_sensor_update=False) + scene_cfg.terrain = FLAT_TERRAIN_CFG.replace(prim_path="/World/ground") + scene_cfg.shape = CUBE_CFG + scene_cfg.contact_sensor = ContactSensorCfg( + prim_path=scene_cfg.shape.prim_path, + track_pose=True, + debug_vis=False, + update_period=0.0, + track_air_time=True, + history_length=3, + ) + scene = InteractiveScene(scene_cfg) + # Play the simulator + sim.reset() + + stage = get_current_stage() + prim_path = scene_cfg.shape.prim_path + prim = stage.GetPrimAtPath(prim_path) + + # Ensure the contact sensor was created properly + contact_sensor = scene["contact_sensor"] + assert contact_sensor is not None, "Contact sensor was not created" + + # Check if the prim has contact report API and verify threshold is close to 0.0 + if prim.HasAPI(PhysxSchema.PhysxContactReportAPI): + cr_api = PhysxSchema.PhysxContactReportAPI.Get(stage, prim.GetPrimPath()) + threshold_attr = cr_api.GetThresholdAttr() + if threshold_attr.IsValid(): + threshold_value = threshold_attr.Get() + assert pytest.approx(threshold_value, abs=1e-6) == 0.0, ( + f"Expected USD threshold to be close to 0.0, but got {threshold_value}" + ) + + +# minor gravity force in -z to ensure object stays on ground plane +@pytest.mark.parametrize("grav_dir", [(-10.0, 0.0, -0.1), (0.0, -10.0, -0.1)]) +@pytest.mark.isaacsim_ci +def test_friction_reporting(setup_simulation, grav_dir): + """ + Test friction force reporting for contact sensors. + + This test places a contact sensor enabled cube onto a ground plane under different gravity directions. + It then compares the normalized friction force dir with the direction of gravity to ensure they are aligned. + """ + sim_dt, _, _, _, carb_settings_iface = setup_simulation + carb_settings_iface.set_bool("/physics/disableContactProcessing", True) + device = "cuda:0" + sim_cfg = SimulationCfg(dt=sim_dt, device=device, gravity=grav_dir) + with build_simulation_context(sim_cfg=sim_cfg, add_lighting=False) as sim: + sim._app_control_on_stop_handle = None + + scene_cfg = ContactSensorSceneCfg(num_envs=1, env_spacing=1.0, lazy_sensor_update=False) + scene_cfg.terrain = FLAT_TERRAIN_CFG + scene_cfg.shape = CUBE_CFG + + filter_prim_paths_expr = [scene_cfg.terrain.prim_path + "/terrain/GroundPlane/CollisionPlane"] + + scene_cfg.contact_sensor = ContactSensorCfg( + prim_path=scene_cfg.shape.prim_path, + track_pose=True, + debug_vis=False, + update_period=0.0, + track_air_time=True, + history_length=3, + track_friction_forces=True, + filter_prim_paths_expr=filter_prim_paths_expr, + ) + + scene = InteractiveScene(scene_cfg) + + sim.reset() + + scene["contact_sensor"].reset() + scene["shape"].write_root_pose_to_sim( + root_pose=torch.tensor([0, 0.0, CUBE_CFG.spawn.size[2] / 2.0, 1, 0, 0, 0]) + ) + + # step sim once to compute friction forces + _perform_sim_step(sim, scene, sim_dt) + + # check that forces are being reported match expected friction forces + expected_friction, _, _, _ = scene["contact_sensor"].contact_physx_view.get_friction_data(dt=sim_dt) + reported_friction = scene["contact_sensor"].data.friction_forces_w[0, 0, :] + + torch.testing.assert_close(expected_friction.sum(dim=0), reported_friction[0], atol=1e-6, rtol=1e-5) + + # check that friction force direction opposes gravity direction + grav = torch.tensor(grav_dir, device=device) + norm_reported_friction = reported_friction / reported_friction.norm() + norm_gravity = grav / grav.norm() + dot = torch.dot(norm_reported_friction[0], norm_gravity) + + torch.testing.assert_close(torch.abs(dot), torch.tensor(1.0, device=device), atol=1e-4, rtol=1e-3) + + +@pytest.mark.isaacsim_ci +def test_invalid_prim_paths_config(setup_simulation): + sim_dt, _, _, _, carb_settings_iface = setup_simulation + carb_settings_iface.set_bool("/physics/disableContactProcessing", True) + device = "cuda:0" + sim_cfg = SimulationCfg(dt=sim_dt, device=device) + with build_simulation_context(sim_cfg=sim_cfg, add_lighting=False) as sim: + sim._app_control_on_stop_handle = None + + scene_cfg = ContactSensorSceneCfg(num_envs=1, env_spacing=1.0, lazy_sensor_update=False) + scene_cfg.terrain = FLAT_TERRAIN_CFG + scene_cfg.shape = CUBE_CFG + + scene_cfg.contact_sensor = ContactSensorCfg( + prim_path=scene_cfg.shape.prim_path, + track_pose=True, + debug_vis=False, + update_period=0.0, + track_air_time=True, + history_length=3, + track_friction_forces=True, + filter_prim_paths_expr=[], + ) + + try: + _ = InteractiveScene(scene_cfg) + + sim.reset() + + assert False, "Expected ValueError due to invalid contact sensor configuration." + except ValueError: + pass + + +@pytest.mark.isaacsim_ci +def test_invalid_max_contact_points_config(setup_simulation): + sim_dt, _, _, _, carb_settings_iface = setup_simulation + carb_settings_iface.set_bool("/physics/disableContactProcessing", True) + device = "cuda:0" + sim_cfg = SimulationCfg(dt=sim_dt, device=device) + with build_simulation_context(sim_cfg=sim_cfg, add_lighting=False) as sim: + sim._app_control_on_stop_handle = None + + scene_cfg = ContactSensorSceneCfg(num_envs=1, env_spacing=1.0, lazy_sensor_update=False) + scene_cfg.terrain = FLAT_TERRAIN_CFG + scene_cfg.shape = CUBE_CFG + filter_prim_paths_expr = [scene_cfg.terrain.prim_path + "/terrain/GroundPlane/CollisionPlane"] + + scene_cfg.contact_sensor = ContactSensorCfg( + prim_path=scene_cfg.shape.prim_path, + track_pose=True, + debug_vis=False, + update_period=0.0, + track_air_time=True, + history_length=3, + track_friction_forces=True, + filter_prim_paths_expr=filter_prim_paths_expr, + max_contact_data_count_per_prim=0, + ) + + try: + _ = InteractiveScene(scene_cfg) + + sim.reset() + + assert False, "Expected ValueError due to invalid contact sensor configuration." + except ValueError: + pass + + """ Internal helpers. """ @@ -416,20 +591,20 @@ def _run_contact_sensor_test( """ for device in devices: for terrain in terrains: - for track_contact_points in [True, False]: + for track_contact_data in [True, False]: with build_simulation_context(device=device, dt=sim_dt, add_lighting=True) as sim: sim._app_control_on_stop_handle = None scene_cfg = ContactSensorSceneCfg(num_envs=1, env_spacing=1.0, lazy_sensor_update=False) scene_cfg.terrain = terrain scene_cfg.shape = shape_cfg - test_contact_position = False + test_contact_data = False if (type(shape_cfg.spawn) is sim_utils.SphereCfg) and (terrain.terrain_type == "plane"): - test_contact_position = True - elif track_contact_points: + test_contact_data = True + elif track_contact_data: continue - if track_contact_points: + if track_contact_data: if terrain.terrain_type == "plane": filter_prim_paths_expr = [terrain.prim_path + "/terrain/GroundPlane/CollisionPlane"] elif terrain.terrain_type == "generator": @@ -444,7 +619,8 @@ def _run_contact_sensor_test( update_period=0.0, track_air_time=True, history_length=3, - track_contact_points=track_contact_points, + track_contact_points=track_contact_data, + track_friction_forces=track_contact_data, filter_prim_paths_expr=filter_prim_paths_expr, ) scene = InteractiveScene(scene_cfg) @@ -461,7 +637,7 @@ def _run_contact_sensor_test( scene=scene, sim_dt=sim_dt, durations=durations, - test_contact_position=test_contact_position, + test_contact_data=test_contact_data, ) _test_sensor_contact( shape=scene["shape"], @@ -471,7 +647,7 @@ def _run_contact_sensor_test( scene=scene, sim_dt=sim_dt, durations=durations, - test_contact_position=test_contact_position, + test_contact_data=test_contact_data, ) @@ -483,7 +659,7 @@ def _test_sensor_contact( scene: InteractiveScene, sim_dt: float, durations: list[float], - test_contact_position: bool = False, + test_contact_data: bool = False, ): """Test for the contact sensor. @@ -550,8 +726,11 @@ def _test_sensor_contact( expected_last_air_time=expected_last_test_contact_time, dt=duration + sim_dt, ) - if test_contact_position: + + if test_contact_data: _test_contact_position(shape, sensor, mode) + _test_friction_forces(shape, sensor, mode) + # switch the contact mode for 1 dt step before the next contact test begins. shape.write_root_pose_to_sim(root_pose=reset_pose) # perform simulation step @@ -562,6 +741,33 @@ def _test_sensor_contact( expected_last_reset_contact_time = 2 * sim_dt +def _test_friction_forces(shape: RigidObject, sensor: ContactSensor, mode: ContactTestMode) -> None: + if not sensor.cfg.track_friction_forces: + assert sensor._data.friction_forces_w is None + return + + # check shape of the contact_pos_w tensor + num_bodies = sensor.num_bodies + assert sensor._data.friction_forces_w.shape == (sensor.num_instances // num_bodies, num_bodies, 1, 3) + # compare friction forces + if mode == ContactTestMode.IN_CONTACT: + assert torch.any(torch.abs(sensor._data.friction_forces_w) > 1e-5).item() + friction_forces, _, buffer_count, buffer_start_indices = sensor.contact_physx_view.get_friction_data( + dt=sensor._sim_physics_dt + ) + for i in range(sensor.num_instances * num_bodies): + for j in range(sensor.contact_physx_view.filter_count): + start_index_ij = buffer_start_indices[i, j] + count_ij = buffer_count[i, j] + force = torch.sum(friction_forces[start_index_ij : (start_index_ij + count_ij), :], dim=0) + env_idx = i // num_bodies + body_idx = i % num_bodies + assert torch.allclose(force, sensor._data.friction_forces_w[env_idx, body_idx, j, :], atol=1e-5) + + elif mode == ContactTestMode.NON_CONTACT: + assert torch.all(sensor._data.friction_forces_w == 0.0).item() + + def _test_contact_position(shape: RigidObject, sensor: ContactSensor, mode: ContactTestMode) -> None: """Test for the contact positions (only implemented for sphere and flat terrain) checks that the contact position is radius distance away from the root of the object @@ -570,22 +776,23 @@ def _test_contact_position(shape: RigidObject, sensor: ContactSensor, mode: Cont sensor: The sensor reporting data to be verified by the contact sensor test. mode: The contact test mode: either contact with ground plane or air time. """ - if sensor.cfg.track_contact_points: - # check shape of the contact_pos_w tensor - num_bodies = sensor.num_bodies - assert sensor._data.contact_pos_w.shape == (sensor.num_instances / num_bodies, num_bodies, 1, 3) - # check contact positions - if mode == ContactTestMode.IN_CONTACT: - contact_position = sensor._data.pos_w + torch.tensor( - [[0.0, 0.0, -shape.cfg.spawn.radius]], device=sensor._data.pos_w.device - ) - assert torch.all( - torch.abs(torch.norm(sensor._data.contact_pos_w - contact_position.unsqueeze(1), p=2, dim=-1)) < 1e-2 - ).item() - elif mode == ContactTestMode.NON_CONTACT: - assert torch.all(torch.isnan(sensor._data.contact_pos_w)).item() - else: + if not sensor.cfg.track_contact_points: assert sensor._data.contact_pos_w is None + return + + # check shape of the contact_pos_w tensor + num_bodies = sensor.num_bodies + assert sensor._data.contact_pos_w.shape == (sensor.num_instances // num_bodies, num_bodies, 1, 3) + # check contact positions + if mode == ContactTestMode.IN_CONTACT: + contact_position = sensor._data.pos_w + torch.tensor( + [[0.0, 0.0, -shape.cfg.spawn.radius]], device=sensor._data.pos_w.device + ) + assert torch.all( + torch.abs(torch.norm(sensor._data.contact_pos_w - contact_position.unsqueeze(1), p=2, dim=-1)) < 1e-2 + ).item() + elif mode == ContactTestMode.NON_CONTACT: + assert torch.all(torch.isnan(sensor._data.contact_pos_w)).item() def _check_prim_contact_state_times( diff --git a/source/isaaclab/test/sensors/test_frame_transformer.py b/source/isaaclab/test/sensors/test_frame_transformer.py index eda9f6019abb..5e0ccf8e1f35 100644 --- a/source/isaaclab/test/sensors/test_frame_transformer.py +++ b/source/isaaclab/test/sensors/test_frame_transformer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +13,10 @@ """Rest everything follows.""" import math -import scipy.spatial.transform as tf -import torch -import isaacsim.core.utils.stage as stage_utils import pytest +import scipy.spatial.transform as tf +import torch import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils @@ -76,7 +75,7 @@ class MySceneCfg(InteractiveSceneCfg): def sim(): """Create a simulation context.""" # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Load kit helper sim = sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.005, device="cpu")) # Set main camera @@ -589,3 +588,209 @@ def test_sensor_print(sim): sim.reset() # print info print(scene.sensors["frame_transformer"]) + + +@pytest.mark.isaacsim_ci +@pytest.mark.parametrize("source_robot", ["Robot", "Robot_1"]) +@pytest.mark.parametrize("path_prefix", ["{ENV_REGEX_NS}", "/World"]) +def test_frame_transformer_duplicate_body_names(sim, source_robot, path_prefix): + """Test tracking bodies with same leaf name at different hierarchy levels. + + This test verifies that bodies with the same leaf name but different paths + (e.g., Robot/LF_SHANK vs Robot_1/LF_SHANK, or arm/link vs leg/link) are tracked + separately using their full relative paths internally. + + The test uses 4 target frames to cover both scenarios: + + Explicit frame names (recommended when bodies share the same leaf name): + User provides unique names like "Robot_LF_SHANK" and "Robot_1_LF_SHANK" to + distinguish between bodies at different hierarchy levels. This makes it + easy to identify which transform belongs to which body. + + Implicit frame names (backward compatibility): + When no name is provided, it defaults to the leaf body name (e.g., "RF_SHANK"). + This preserves backward compatibility for users who may have existing code like + `idx = target_frame_names.index("RF_SHANK")`. However, when multiple bodies share + the same leaf name, this results in duplicate frame names. The transforms are + still distinct because internal body tracking uses full relative paths. + + Args: + source_robot: The robot to use as the source frame ("Robot" or "Robot_1"). + This tests that both source frames work correctly when there are + duplicate body names. + path_prefix: The path prefix to use ("{ENV_REGEX_NS}" for env patterns or "/World" for direct paths). + """ + + # Create a custom scene config with two robots + @configclass + class MultiRobotSceneCfg(InteractiveSceneCfg): + """Scene with two robots having bodies with same names.""" + + terrain = TerrainImporterCfg(prim_path="/World/ground", terrain_type="plane") + + # Frame transformer will be set after config creation (needs source_robot parameter) + frame_transformer: FrameTransformerCfg = None # type: ignore + + # Use multiple envs for env patterns, single env for direct paths + num_envs = 2 if path_prefix == "{ENV_REGEX_NS}" else 1 + env_spacing = 10.0 if path_prefix == "{ENV_REGEX_NS}" else 0.0 + + # Create scene config with appropriate prim paths + scene_cfg = MultiRobotSceneCfg(num_envs=num_envs, env_spacing=env_spacing, lazy_sensor_update=False) + scene_cfg.robot = ANYMAL_C_CFG.replace(prim_path=f"{path_prefix}/Robot") + scene_cfg.robot_1 = ANYMAL_C_CFG.replace( + prim_path=f"{path_prefix}/Robot_1", + init_state=ANYMAL_C_CFG.init_state.replace(pos=(2.0, 0.0, 0.6)), + ) + + # Frame transformer tracking same-named bodies from both robots + # Source frame is parametrized to test both Robot/base and Robot_1/base + scene_cfg.frame_transformer = FrameTransformerCfg( + prim_path=f"{path_prefix}/{source_robot}/base", + target_frames=[ + # Explicit frame names (recommended when bodies share the same leaf name) + FrameTransformerCfg.FrameCfg( + name="Robot_LF_SHANK", + prim_path=f"{path_prefix}/Robot/LF_SHANK", + ), + FrameTransformerCfg.FrameCfg( + name="Robot_1_LF_SHANK", + prim_path=f"{path_prefix}/Robot_1/LF_SHANK", + ), + # Implicit frame names (backward compatibility) + FrameTransformerCfg.FrameCfg( + prim_path=f"{path_prefix}/Robot/RF_SHANK", + ), + FrameTransformerCfg.FrameCfg( + prim_path=f"{path_prefix}/Robot_1/RF_SHANK", + ), + ], + ) + scene = InteractiveScene(scene_cfg) + + # Play the simulator + sim.reset() + + # Get target frame names + target_frame_names = scene.sensors["frame_transformer"].data.target_frame_names + + # Verify explicit frame names are present + assert "Robot_LF_SHANK" in target_frame_names, f"Expected 'Robot_LF_SHANK', got {target_frame_names}" + assert "Robot_1_LF_SHANK" in target_frame_names, f"Expected 'Robot_1_LF_SHANK', got {target_frame_names}" + + # Without explicit names, both RF_SHANK frames default to same name "RF_SHANK" + # This results in duplicate frame names (expected behavior for backwards compatibility) + rf_shank_count = target_frame_names.count("RF_SHANK") + assert rf_shank_count == 2, f"Expected 2 'RF_SHANK' entries (name collision), got {rf_shank_count}" + + # Get indices for explicit named frames + robot_lf_idx = target_frame_names.index("Robot_LF_SHANK") + robot_1_lf_idx = target_frame_names.index("Robot_1_LF_SHANK") + + # Get indices for implicit named frames (both named "RF_SHANK") + rf_shank_indices = [i for i, name in enumerate(target_frame_names) if name == "RF_SHANK"] + assert len(rf_shank_indices) == 2, f"Expected 2 RF_SHANK indices, got {rf_shank_indices}" + + # Acquire ground truth body indices + robot_base_body_idx = scene.articulations["robot"].find_bodies("base")[0][0] + robot_1_base_body_idx = scene.articulations["robot_1"].find_bodies("base")[0][0] + robot_lf_shank_body_idx = scene.articulations["robot"].find_bodies("LF_SHANK")[0][0] + robot_1_lf_shank_body_idx = scene.articulations["robot_1"].find_bodies("LF_SHANK")[0][0] + robot_rf_shank_body_idx = scene.articulations["robot"].find_bodies("RF_SHANK")[0][0] + robot_1_rf_shank_body_idx = scene.articulations["robot_1"].find_bodies("RF_SHANK")[0][0] + + # Determine expected source frame based on parameter + expected_source_robot = "robot" if source_robot == "Robot" else "robot_1" + expected_source_base_body_idx = robot_base_body_idx if source_robot == "Robot" else robot_1_base_body_idx + + # Define simulation stepping + sim_dt = sim.get_physics_dt() + + # Simulate physics + for count in range(20): + # Reset periodically + if count % 10 == 0: + # Reset robot + root_state = scene.articulations["robot"].data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + scene.articulations["robot"].write_root_pose_to_sim(root_state[:, :7]) + scene.articulations["robot"].write_root_velocity_to_sim(root_state[:, 7:]) + scene.articulations["robot"].write_joint_state_to_sim( + scene.articulations["robot"].data.default_joint_pos, + scene.articulations["robot"].data.default_joint_vel, + ) + # Reset robot_1 + root_state_1 = scene.articulations["robot_1"].data.default_root_state.clone() + root_state_1[:, :3] += scene.env_origins + scene.articulations["robot_1"].write_root_pose_to_sim(root_state_1[:, :7]) + scene.articulations["robot_1"].write_root_velocity_to_sim(root_state_1[:, 7:]) + scene.articulations["robot_1"].write_joint_state_to_sim( + scene.articulations["robot_1"].data.default_joint_pos, + scene.articulations["robot_1"].data.default_joint_vel, + ) + scene.reset() + + # Write data to sim + scene.write_data_to_sim() + # Perform step + sim.step() + # Read data from sim + scene.update(sim_dt) + + # Get frame transformer data + frame_transformer_data = scene.sensors["frame_transformer"].data + source_pos_w = frame_transformer_data.source_pos_w + source_quat_w = frame_transformer_data.source_quat_w + target_pos_w = frame_transformer_data.target_pos_w + + # Get ground truth positions and orientations (after scene.update() so they're current) + robot_lf_pos_w = scene.articulations["robot"].data.body_pos_w[:, robot_lf_shank_body_idx] + robot_1_lf_pos_w = scene.articulations["robot_1"].data.body_pos_w[:, robot_1_lf_shank_body_idx] + robot_rf_pos_w = scene.articulations["robot"].data.body_pos_w[:, robot_rf_shank_body_idx] + robot_1_rf_pos_w = scene.articulations["robot_1"].data.body_pos_w[:, robot_1_rf_shank_body_idx] + + # Get expected source frame positions and orientations (after scene.update() so they're current) + expected_source_base_pos_w = scene.articulations[expected_source_robot].data.body_pos_w[ + :, expected_source_base_body_idx + ] + expected_source_base_quat_w = scene.articulations[expected_source_robot].data.body_quat_w[ + :, expected_source_base_body_idx + ] + + # TEST 1: Verify source frame is correctly resolved + # The source_pos_w should match the expected source robot's base world position + torch.testing.assert_close(source_pos_w, expected_source_base_pos_w, rtol=1e-5, atol=1e-5) + torch.testing.assert_close(source_quat_w, expected_source_base_quat_w, rtol=1e-5, atol=1e-5) + + # TEST 2: Explicit named frames (LF_SHANK) should have DIFFERENT world positions + lf_pos_difference = torch.norm(target_pos_w[:, robot_lf_idx] - target_pos_w[:, robot_1_lf_idx], dim=-1) + assert torch.all(lf_pos_difference > 1.0), ( + f"Robot_LF_SHANK and Robot_1_LF_SHANK should have different positions (got diff={lf_pos_difference}). " + "This indicates body name collision bug." + ) + + # Verify explicit named frames match correct robot bodies + torch.testing.assert_close(target_pos_w[:, robot_lf_idx], robot_lf_pos_w) + torch.testing.assert_close(target_pos_w[:, robot_1_lf_idx], robot_1_lf_pos_w) + + # TEST 3: Implicit named frames (RF_SHANK) should also have DIFFERENT world positions + # Even though they have the same frame name, internal body tracking uses full paths + rf_pos_difference = torch.norm( + target_pos_w[:, rf_shank_indices[0]] - target_pos_w[:, rf_shank_indices[1]], dim=-1 + ) + assert torch.all(rf_pos_difference > 1.0), ( + f"The two RF_SHANK frames should have different positions (got diff={rf_pos_difference}). " + "This indicates body name collision bug in internal body tracking." + ) + + # Verify implicit named frames match correct robot bodies + # Note: Order depends on internal processing, so we check both match one of the robots + rf_positions = [target_pos_w[:, rf_shank_indices[0]], target_pos_w[:, rf_shank_indices[1]]] + + # Each tracked position should match one of the ground truth positions + for rf_pos in rf_positions: + matches_robot = torch.allclose(rf_pos, robot_rf_pos_w, atol=1e-5) + matches_robot_1 = torch.allclose(rf_pos, robot_1_rf_pos_w, atol=1e-5) + assert matches_robot or matches_robot_1, ( + f"RF_SHANK position {rf_pos} doesn't match either robot's RF_SHANK position" + ) diff --git a/source/isaaclab/test/sensors/test_imu.py b/source/isaaclab/test/sensors/test_imu.py index 7f621a365747..92c97f0c6d70 100644 --- a/source/isaaclab/test/sensors/test_imu.py +++ b/source/isaaclab/test/sensors/test_imu.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,14 +11,12 @@ app_launcher = AppLauncher(headless=True, enable_cameras=True) simulation_app = app_launcher.app - """Rest everything follows.""" import pathlib -import torch -import isaacsim.core.utils.stage as stage_utils import pytest +import torch import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils @@ -26,7 +24,7 @@ from isaaclab.assets import ArticulationCfg, RigidObjectCfg from isaaclab.markers.config import GREEN_ARROW_X_MARKER_CFG, RED_ARROW_X_MARKER_CFG from isaaclab.scene import InteractiveScene, InteractiveSceneCfg -from isaaclab.sensors.imu import ImuCfg +from isaaclab.sensors.imu import Imu, ImuCfg from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass @@ -83,6 +81,7 @@ class MySceneCfg(InteractiveSceneCfg): # articulations - robot robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/robot") + # pendulum1 pendulum = ArticulationCfg( prim_path="{ENV_REGEX_NS}/pendulum", spawn=sim_utils.UrdfFileCfg( @@ -102,6 +101,27 @@ class MySceneCfg(InteractiveSceneCfg): "joint_1_act": ImplicitActuatorCfg(joint_names_expr=["joint_.*"], stiffness=0.0, damping=0.3), }, ) + # pendulum2 + pendulum2 = ArticulationCfg( + prim_path="{ENV_REGEX_NS}/pendulum2", + spawn=sim_utils.UrdfFileCfg( + fix_base=True, + merge_fixed_joints=True, + make_instanceable=False, + asset_path=f"{pathlib.Path(__file__).parent.resolve()}/urdfs/simple_2_link.urdf", + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + joint_drive=sim_utils.UrdfConverterCfg.JointDriveCfg( + gains=sim_utils.UrdfConverterCfg.JointDriveCfg.PDGainsCfg(stiffness=None, damping=None) + ), + ), + init_state=ArticulationCfg.InitialStateCfg(), + actuators={ + "joint_1_act": ImplicitActuatorCfg(joint_names_expr=["joint_.*"], stiffness=0.0, damping=0.3), + }, + ) + # sensors - imu (filled inside unit test) imu_ball: ImuCfg = ImuCfg( prim_path="{ENV_REGEX_NS}/ball", @@ -123,7 +143,30 @@ class MySceneCfg(InteractiveSceneCfg): ), gravity_bias=(0.0, 0.0, 0.0), ) - + imu_robot_norb: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/robot/LF_HIP/LF_hip_fixed", + offset=ImuCfg.OffsetCfg( + pos=POS_OFFSET, + rot=ROT_OFFSET, + ), + gravity_bias=(0.0, 0.0, 0.0), + ) + imu_indirect_pendulum_link: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/pendulum2/link_1/imu_link", + debug_vis=not app_launcher._headless, + visualizer_cfg=RED_ARROW_X_MARKER_CFG.replace(prim_path="/Visuals/Acceleration/imu_link"), + gravity_bias=(0.0, 0.0, 9.81), + ) + imu_indirect_pendulum_base: ImuCfg = ImuCfg( + prim_path="{ENV_REGEX_NS}/pendulum2/link_1", + offset=ImuCfg.OffsetCfg( + pos=PEND_POS_OFFSET, + rot=PEND_ROT_OFFSET, + ), + debug_vis=not app_launcher._headless, + visualizer_cfg=GREEN_ARROW_X_MARKER_CFG.replace(prim_path="/Visuals/Acceleration/base"), + gravity_bias=(0.0, 0.0, 9.81), + ) imu_pendulum_imu_link: ImuCfg = ImuCfg( prim_path="{ENV_REGEX_NS}/pendulum/imu_link", debug_vis=not app_launcher._headless, @@ -145,7 +188,8 @@ def __post_init__(self): """Post initialization.""" # change position of the robot self.robot.init_state.pos = (0.0, 2.0, 1.0) - self.pendulum.init_state.pos = (-1.0, 1.0, 0.5) + self.pendulum.init_state.pos = (-2.0, 1.0, 0.5) + self.pendulum2.init_state.pos = (2.0, 1.0, 0.5) # change asset self.robot.spawn.usd_path = f"{ISAAC_NUCLEUS_DIR}/Robots/ANYbotics/anymal_c/anymal_c.usd" @@ -158,7 +202,7 @@ def __post_init__(self): def setup_sim(): """Create a simulation context and scene.""" # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Load simulation context sim_cfg = sim_utils.SimulationCfg(dt=0.001) sim_cfg.physx.solver_type = 0 # 0: PGS, 1: TGS --> use PGS for more accurate results @@ -316,7 +360,6 @@ def test_single_dof_pendulum(setup_sim): # should achieve same results between the two imu sensors on the robot for idx in range(500): - # write data to sim scene.write_data_to_sim() # perform step @@ -441,10 +484,152 @@ def test_single_dof_pendulum(setup_sim): ) +@pytest.mark.isaacsim_ci +def test_indirect_attachment(setup_sim): + """Test attaching the imu through an xForm primitive configuration argument.""" + sim, scene = setup_sim + # pendulum length + pend_length = PEND_POS_OFFSET[0] + + # should achieve same results between the two imu sensors on the robot + for idx in range(500): + # write data to sim + scene.write_data_to_sim() + # perform step + sim.step() + # read data from sim + scene.update(sim.get_physics_dt()) + + # get pendulum joint state + joint_pos = scene.articulations["pendulum2"].data.joint_pos + joint_vel = scene.articulations["pendulum2"].data.joint_vel + joint_acc = scene.articulations["pendulum2"].data.joint_acc + + imu = scene.sensors["imu_indirect_pendulum_link"] + imu_base = scene.sensors["imu_indirect_pendulum_base"] + + torch.testing.assert_close( + imu._offset_pos_b, + imu_base._offset_pos_b, + ) + torch.testing.assert_close(imu._offset_quat_b, imu_base._offset_quat_b, rtol=1e-4, atol=1e-4) + + # IMU and base data + imu_data = scene.sensors["imu_indirect_pendulum_link"].data + base_data = scene.sensors["imu_indirect_pendulum_base"].data + # extract imu_link imu_sensor dynamics + lin_vel_w_imu_link = math_utils.quat_apply(imu_data.quat_w, imu_data.lin_vel_b) + lin_acc_w_imu_link = math_utils.quat_apply(imu_data.quat_w, imu_data.lin_acc_b) + + # calculate the joint dynamics from the imu_sensor (y axis of imu_link is parallel to joint axis of pendulum) + joint_vel_imu = math_utils.quat_apply(imu_data.quat_w, imu_data.ang_vel_b)[..., 1].unsqueeze(-1) + joint_acc_imu = math_utils.quat_apply(imu_data.quat_w, imu_data.ang_acc_b)[..., 1].unsqueeze(-1) + + # calculate analytical solution + vx = -joint_vel * pend_length * torch.sin(joint_pos) + vy = torch.zeros(2, 1, device=scene.device) + vz = -joint_vel * pend_length * torch.cos(joint_pos) + gt_linear_vel_w = torch.cat([vx, vy, vz], dim=-1) + + ax = -joint_acc * pend_length * torch.sin(joint_pos) - joint_vel**2 * pend_length * torch.cos(joint_pos) + ay = torch.zeros(2, 1, device=scene.device) + az = -joint_acc * pend_length * torch.cos(joint_pos) + joint_vel**2 * pend_length * torch.sin(joint_pos) + 9.81 + gt_linear_acc_w = torch.cat([ax, ay, az], dim=-1) + + # skip first step where initial velocity is zero + if idx < 2: + continue + + # compare imu projected gravity + gravity_dir_w = torch.tensor((0.0, 0.0, -1.0), device=scene.device).repeat(2, 1) + gravity_dir_b = math_utils.quat_apply_inverse(imu_data.quat_w, gravity_dir_w) + torch.testing.assert_close( + imu_data.projected_gravity_b, + gravity_dir_b, + ) + + # compare imu angular velocity with joint velocity + torch.testing.assert_close( + joint_vel, + joint_vel_imu, + rtol=1e-1, + atol=1e-3, + ) + # compare imu angular acceleration with joint acceleration + torch.testing.assert_close( + joint_acc, + joint_acc_imu, + rtol=1e-1, + atol=1e-3, + ) + # compare imu linear velocity with simple pendulum calculation + torch.testing.assert_close( + gt_linear_vel_w, + lin_vel_w_imu_link, + rtol=1e-1, + atol=1e-3, + ) + # compare imu linear acceleration with simple pendulum calculation + torch.testing.assert_close( + gt_linear_acc_w, + lin_acc_w_imu_link, + rtol=1e-1, + atol=1e0, + ) + + # check the position between offset and imu definition + torch.testing.assert_close( + base_data.pos_w, + imu_data.pos_w, + rtol=1e-5, + atol=1e-5, + ) + + # check the orientation between offset and imu definition + torch.testing.assert_close( + base_data.quat_w, + imu_data.quat_w, + rtol=1e-4, + atol=1e-4, + ) + + # check the angular velocities of the imus between offset and imu definition + torch.testing.assert_close( + base_data.ang_vel_b, + imu_data.ang_vel_b, + rtol=1e-4, + atol=1e-4, + ) + # check the angular acceleration of the imus between offset and imu definition + torch.testing.assert_close( + base_data.ang_acc_b, + imu_data.ang_acc_b, + rtol=1e-4, + atol=1e-4, + ) + + # check the linear velocity of the imus between offset and imu definition + torch.testing.assert_close( + base_data.lin_vel_b, + imu_data.lin_vel_b, + rtol=1e-2, + atol=5e-3, + ) + + # check the linear acceleration of the imus between offset and imu definition + torch.testing.assert_close( + base_data.lin_acc_b, + imu_data.lin_acc_b, + rtol=1e-1, + atol=1e-1, + ) + + @pytest.mark.isaacsim_ci def test_offset_calculation(setup_sim): """Test offset configuration argument.""" sim, scene = setup_sim + # should achieve same results between the two imu sensors on the robot for idx in range(500): # set acceleration @@ -516,6 +701,21 @@ def test_offset_calculation(setup_sim): ) +@pytest.mark.isaacsim_ci +def test_attachment_validity(setup_sim): + """Test invalid imu attachment. An imu cannot be attached directly to the world. It must be somehow attached to + something implementing physics.""" + sim, scene = setup_sim + imu_world_cfg = ImuCfg( + prim_path="/World/envs/env_0", + gravity_bias=(0.0, 0.0, 0.0), + ) + with pytest.raises(RuntimeError) as exc_info: + imu_world = Imu(imu_world_cfg) + imu_world._initialize_impl() + assert exc_info.type is RuntimeError and "find a rigid body ancestor prim" in str(exc_info.value) + + @pytest.mark.isaacsim_ci def test_env_ids_propagation(setup_sim): """Test that env_ids argument propagates through update and reset methods""" diff --git a/source/isaaclab/test/sensors/test_multi_mesh_ray_caster.py b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster.py new file mode 100644 index 000000000000..c27b25b53b79 --- /dev/null +++ b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster.py @@ -0,0 +1,250 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from __future__ import annotations + +from isaaclab.app import AppLauncher + +# launch omniverse app. Used for warp. +app_launcher = AppLauncher(headless=True) + +import numpy as np +import pytest +import torch +import trimesh +import warp as wp + +from isaaclab.utils.math import matrix_from_quat, quat_from_euler_xyz, random_orientation +from isaaclab.utils.warp.ops import convert_to_warp_mesh, raycast_dynamic_meshes, raycast_single_mesh + + +@pytest.fixture(scope="module") +def device(): + return "cuda" if torch.cuda.is_available() else "cpu" + + +@pytest.fixture +def rays(device): + ray_starts = torch.tensor([[0, -0.35, -5], [0.25, 0.35, -5]], dtype=torch.float32, device=device).unsqueeze(0) + ray_directions = torch.tensor([[0, 0, 1], [0, 0, 1]], dtype=torch.float32, device=device).unsqueeze(0) + expected_ray_hits = torch.tensor( + [[0, -0.35, -0.5], [0.25, 0.35, -0.5]], dtype=torch.float32, device=device + ).unsqueeze(0) + return ray_starts, ray_directions, expected_ray_hits + + +@pytest.fixture +def trimesh_box(): + return trimesh.creation.box([2, 2, 1]) + + +@pytest.fixture +def single_mesh(trimesh_box, device): + wp_mesh = convert_to_warp_mesh(trimesh_box.vertices, trimesh_box.faces, device) + return wp_mesh, wp_mesh.id + + +def test_raycast_multi_cubes(device, trimesh_box, rays): + """Test raycasting against two cubes.""" + ray_starts, ray_directions, _ = rays + + trimesh_1 = trimesh_box.copy() + wp_mesh_1 = convert_to_warp_mesh(trimesh_1.vertices, trimesh_1.faces, device) + + translation = np.eye(4) + translation[:3, 3] = [0, 2, 0] + trimesh_2 = trimesh_box.copy().apply_transform(translation) + wp_mesh_2 = convert_to_warp_mesh(trimesh_2.vertices, trimesh_2.faces, device) + + # get mesh id array + mesh_ids_wp = wp.array2d([[wp_mesh_1.id, wp_mesh_2.id]], dtype=wp.uint64, device=device) + + # Static positions (no transforms passed) + ray_start = torch.tensor([[0, 0, -5], [0, 2.5, -5]], dtype=torch.float32, device=device).unsqueeze(0) + ray_hits, ray_distance, ray_normal, ray_face_id, mesh_ids = raycast_dynamic_meshes( + ray_start, + ray_directions, + mesh_ids_wp, + return_distance=True, + return_normal=True, + return_face_id=True, + return_mesh_id=True, + ) + + torch.testing.assert_close( + ray_hits, torch.tensor([[[0, 0, -0.5], [0, 2.5, -0.5]]], dtype=torch.float32, device=device) + ) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], dtype=torch.float32, device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], dtype=torch.float32, device=device)) + assert torch.equal(mesh_ids, torch.tensor([[0, 1]], dtype=torch.int32, device=device)) + + # Dynamic positions/orientations + ray_start = torch.tensor([[0, 0, -5], [0, 4.5, -5]], dtype=torch.float32, device=device).unsqueeze(0) + ray_hits, ray_distance, ray_normal, ray_face_id, mesh_ids = raycast_dynamic_meshes( + ray_start, + ray_directions, + mesh_ids_wp, + return_distance=True, + return_normal=True, + return_face_id=True, + mesh_positions_w=torch.tensor([[[0, 0, 0], [0, 2, 0]]], dtype=torch.float32, device=device), + mesh_orientations_w=torch.tensor([[[1, 0, 0, 0], [1, 0, 0, 0]]], dtype=torch.float32, device=device), + return_mesh_id=True, + ) + + torch.testing.assert_close( + ray_hits, torch.tensor([[[0, 0, -0.5], [0, 4.5, -0.5]]], dtype=torch.float32, device=device) + ) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], dtype=torch.float32, device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], dtype=torch.float32, device=device)) + assert torch.equal(mesh_ids, torch.tensor([[0, 1]], dtype=torch.int32, device=device)) + + +def test_raycast_single_cube(device, single_mesh, rays): + """Test raycasting against a single cube.""" + ray_starts, ray_directions, expected_ray_hits = rays + _, single_mesh_id = single_mesh + + ray_hits, ray_distance, ray_normal, ray_face_id = raycast_single_mesh( + ray_starts, + ray_directions, + single_mesh_id, + return_distance=True, + return_normal=True, + return_face_id=True, + ) + torch.testing.assert_close(ray_hits, expected_ray_hits) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], dtype=torch.float32, device=device)) + torch.testing.assert_close( + ray_normal, + torch.tensor([[[0, 0, -1], [0, 0, -1]]], dtype=torch.float32, device=device), + ) + torch.testing.assert_close(ray_face_id, torch.tensor([[3, 8]], dtype=torch.int32, device=device)) + + # check multiple meshes implementation + ray_hits, ray_distance, ray_normal, ray_face_id, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + ) + torch.testing.assert_close(ray_hits, expected_ray_hits) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], dtype=torch.float32, device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], dtype=torch.float32, device=device)) + torch.testing.assert_close(ray_face_id, torch.tensor([[3, 8]], dtype=torch.int32, device=device)) + + +@pytest.mark.parametrize("num_samples", [10]) +def test_raycast_moving_cube(device, single_mesh, rays, num_samples): + r"""Test raycasting against a single cube with different distances. + |-------------| + |\ | + | \ | + | \ 8 | + | \ | + | \ x_1 | + | \ | + | \ | + | \ | + | \ | + | \ | + | 3 x_2 \ | + | \ | + | \| + |-------------| + + """ + ray_starts, ray_directions, expected_ray_hits = rays + _, single_mesh_id = single_mesh + + # move the cube along the z axis + for distance in torch.linspace(0, 1, num_samples, device=device): + ray_hits, ray_distance, ray_normal, ray_face_id, mesh_id = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + return_mesh_id=True, + mesh_positions_w=torch.tensor([[0, 0, distance]], dtype=torch.float32, device=device), + ) + torch.testing.assert_close( + ray_hits, + expected_ray_hits + + torch.tensor([[0, 0, distance], [0, 0, distance]], dtype=torch.float32, device=device).unsqueeze(0), + ) + torch.testing.assert_close( + ray_distance, distance + torch.tensor([[4.5, 4.5]], dtype=torch.float32, device=device) + ) + torch.testing.assert_close( + ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], dtype=torch.float32, device=device) + ) + torch.testing.assert_close(ray_face_id, torch.tensor([[3, 8]], dtype=torch.int32, device=device)) + + +def test_raycast_rotated_cube(device, single_mesh, rays): + """Test raycasting against a single cube with different 90deg. orientations.""" + ray_starts, ray_directions, expected_ray_hits = rays + _, single_mesh_id = single_mesh + + cube_rotation = quat_from_euler_xyz(torch.tensor([0.0]), torch.tensor([0.0]), torch.tensor([np.pi])).to(device) + ray_hits, ray_distance, ray_normal, ray_face_id, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + mesh_orientations_w=cube_rotation.unsqueeze(0), + ) + torch.testing.assert_close(ray_hits, expected_ray_hits) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], dtype=torch.float32, device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], dtype=torch.float32, device=device)) + # Make sure the face ids are correct. The cube is rotated by 90deg. so the face ids are different. + torch.testing.assert_close(ray_face_id, torch.tensor([[8, 3]], dtype=torch.int32, device=device)) + + +@pytest.mark.parametrize("num_random", [10]) +def test_raycast_random_cube(device, trimesh_box, single_mesh, rays, num_random): + """Test raycasting against a single cube with random poses.""" + ray_starts, ray_directions, _ = rays + _, single_mesh_id = single_mesh + + for orientation in random_orientation(num_random, device): + pos = torch.tensor([[0, 0, torch.rand(1)]], dtype=torch.float32, device=device) + tf_hom = np.eye(4) + tf_hom[:3, :3] = matrix_from_quat(orientation).cpu().numpy() + tf_hom[:3, 3] = pos.cpu().numpy() + tf_mesh = trimesh_box.copy().apply_transform(tf_hom) + + # get raycast for transformed, static mesh + wp_mesh = convert_to_warp_mesh(tf_mesh.vertices, tf_mesh.faces, device) + ray_hits, ray_distance, ray_normal, ray_face_id, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[wp_mesh.id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + ) + # get raycast for modified mesh + ray_hits_m, ray_distance_m, ray_normal_m, ray_face_id_m, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + mesh_positions_w=pos, + mesh_orientations_w=orientation.view(1, 1, -1), + ) + torch.testing.assert_close(ray_hits, ray_hits_m) + torch.testing.assert_close(ray_distance, ray_distance_m) + torch.testing.assert_close(ray_normal, ray_normal_m) + torch.testing.assert_close(ray_face_id, ray_face_id_m) diff --git a/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py new file mode 100644 index 000000000000..6e30a5fcdc98 --- /dev/null +++ b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py @@ -0,0 +1,862 @@ +# 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 + +# ignore private usage of variables warning +# pyright: reportPrivateUsage=none + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True, enable_cameras=True).app + +"""Rest everything follows.""" + +import copy +import os + +import numpy as np +import pytest +import torch + +import omni.replicator.core as rep +from pxr import Gf + +import isaaclab.sim as sim_utils +from isaaclab.sensors.camera import Camera, CameraCfg +from isaaclab.sensors.ray_caster import MultiMeshRayCasterCamera, MultiMeshRayCasterCameraCfg, patterns +from isaaclab.sim import PinholeCameraCfg +from isaaclab.terrains.trimesh.utils import make_plane +from isaaclab.terrains.utils import create_prim_from_mesh +from isaaclab.utils import convert_dict_to_backend +from isaaclab.utils.timer import Timer + +# sample camera poses +POSITION = [2.5, 2.5, 2.5] +QUAT_ROS = [-0.17591989, 0.33985114, 0.82047325, -0.42470819] +QUAT_OPENGL = [0.33985113, 0.17591988, 0.42470818, 0.82047324] +QUAT_WORLD = [-0.3647052, -0.27984815, -0.1159169, 0.88047623] + + +@pytest.fixture(scope="function") +def setup_simulation(): + """Fixture to set up and tear down the simulation environment.""" + # Create a new stage + sim_utils.create_new_stage() + # Simulation time-step + dt = 0.01 + # Load kit helper + sim_cfg = sim_utils.SimulationCfg(dt=dt) + sim: sim_utils.SimulationContext = sim_utils.SimulationContext(sim_cfg) + # Ground-plane + mesh = make_plane(size=(100, 100), height=0.0, center_zero=True) + create_prim_from_mesh("/World/defaultGroundPlane", mesh) + # load stage + sim_utils.update_stage() + + camera_cfg = MultiMeshRayCasterCameraCfg( + prim_path="/World/Camera", + mesh_prim_paths=["/World/defaultGroundPlane"], + update_period=0, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0), convention="world"), + debug_vis=False, + pattern_cfg=patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=480, + width=640, + ), + data_types=["distance_to_image_plane"], + ) + + # create xform because placement of camera directly under world is not supported + sim_utils.create_prim("/World/Camera", "Xform") + + yield sim, dt, camera_cfg + + # Cleanup + # close all the opened viewport from before. + rep.vp_manager.destroy_hydra_textures("Replicator") + # stop simulation + # note: cannot use self.sim.stop() since it does one render step after stopping!! This doesn't make sense :( + sim._timeline.stop() + # clear the stage + sim.clear_all_callbacks() + sim.clear_instance() + + +@pytest.mark.parametrize( + "convention,quat", + [ + ("ros", QUAT_ROS), + ("opengl", QUAT_OPENGL), + ("world", QUAT_WORLD), + ], +) +@pytest.mark.isaacsim_ci +def test_camera_init_offset(setup_simulation, convention, quat): + """Test camera initialization with offset using different conventions.""" + sim, dt, camera_cfg = setup_simulation + + # Create camera config with specific convention + cam_cfg_offset = copy.deepcopy(camera_cfg) + cam_cfg_offset.offset = MultiMeshRayCasterCameraCfg.OffsetCfg( + pos=POSITION, + rot=quat, + convention=convention, + ) + sim_utils.create_prim(f"/World/CameraOffset{convention.capitalize()}", "Xform") + cam_cfg_offset.prim_path = f"/World/CameraOffset{convention.capitalize()}" + + camera = MultiMeshRayCasterCamera(cam_cfg_offset) + + # play sim + sim.reset() + + # update camera + camera.update(dt) + + # check that transform is set correctly + np.testing.assert_allclose(camera.data.pos_w[0].cpu().numpy(), cam_cfg_offset.offset.pos) + + del camera + + +@pytest.mark.isaacsim_ci +def test_camera_init(setup_simulation): + """Test camera initialization.""" + sim, dt, camera_cfg = setup_simulation + + # Create camera + camera = MultiMeshRayCasterCamera(cfg=camera_cfg) + # Play sim + sim.reset() + # Check if camera is initialized + assert camera.is_initialized + # Simulate for a few steps + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + sim.step() + # Check buffers that exists and have correct shapes + assert camera.data.pos_w.shape == (1, 3) + assert camera.data.quat_w_ros.shape == (1, 4) + assert camera.data.quat_w_world.shape == (1, 4) + assert camera.data.quat_w_opengl.shape == (1, 4) + assert camera.data.intrinsic_matrices.shape == (1, 3, 3) + assert camera.data.image_shape == (camera_cfg.pattern_cfg.height, camera_cfg.pattern_cfg.width) + assert camera.data.info == [{camera_cfg.data_types[0]: None}] + # Simulate physics + for _ in range(10): + # perform rendering + sim.step() + # update camera + camera.update(dt) + # check image data + for im_data in camera.data.output.values(): + assert im_data.shape == (1, camera_cfg.pattern_cfg.height, camera_cfg.pattern_cfg.width, 1) + + del camera + + +@pytest.mark.isaacsim_ci +def test_camera_resolution(setup_simulation): + """Test camera resolution is correctly set.""" + sim, dt, camera_cfg = setup_simulation + + # Create camera + camera = MultiMeshRayCasterCamera(cfg=camera_cfg) + # Play sim + sim.reset() + # Simulate for a few steps + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + sim.step() + camera.update(dt) + # access image data and compare shapes + for im_data in camera.data.output.values(): + assert im_data.shape == (1, camera_cfg.pattern_cfg.height, camera_cfg.pattern_cfg.width, 1) + + del camera + + +@pytest.mark.isaacsim_ci +def test_camera_init_intrinsic_matrix(setup_simulation): + """Test camera initialization from intrinsic matrix.""" + sim, dt, camera_cfg = setup_simulation + + # get the first camera + camera_1 = MultiMeshRayCasterCamera(cfg=camera_cfg) + # get intrinsic matrix + sim.reset() + intrinsic_matrix = camera_1.data.intrinsic_matrices[0].cpu().flatten().tolist() + + # initialize from intrinsic matrix + intrinsic_camera_cfg = MultiMeshRayCasterCameraCfg( + prim_path="/World/Camera", + mesh_prim_paths=["/World/defaultGroundPlane"], + update_period=0, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0), convention="world"), + debug_vis=False, + pattern_cfg=patterns.PinholeCameraPatternCfg.from_intrinsic_matrix( + intrinsic_matrix=intrinsic_matrix, + height=camera_cfg.pattern_cfg.height, + width=camera_cfg.pattern_cfg.width, + focal_length=camera_cfg.pattern_cfg.focal_length, + ), + data_types=["distance_to_image_plane"], + ) + camera_2 = MultiMeshRayCasterCamera(cfg=intrinsic_camera_cfg) + + # play sim + sim.reset() + sim.play() + + # update cameras + camera_1.update(dt) + camera_2.update(dt) + + # check image data + torch.testing.assert_close( + camera_1.data.output["distance_to_image_plane"], + camera_2.data.output["distance_to_image_plane"], + ) + # check that both intrinsic matrices are the same + torch.testing.assert_close( + camera_1.data.intrinsic_matrices[0], + camera_2.data.intrinsic_matrices[0], + ) + + del camera_1, camera_2 + + +@pytest.mark.isaacsim_ci +def test_multi_camera_init(setup_simulation): + """Test multi-camera initialization.""" + sim, dt, camera_cfg = setup_simulation + + # -- camera 1 + cam_cfg_1 = copy.deepcopy(camera_cfg) + cam_cfg_1.prim_path = "/World/Camera_0" + sim_utils.create_prim("/World/Camera_0", "Xform") + # Create camera + cam_1 = MultiMeshRayCasterCamera(cam_cfg_1) + + # -- camera 2 + cam_cfg_2 = copy.deepcopy(camera_cfg) + cam_cfg_2.prim_path = "/World/Camera_1" + sim_utils.create_prim("/World/Camera_1", "Xform") + # Create camera + cam_2 = MultiMeshRayCasterCamera(cam_cfg_2) + + # play sim + sim.reset() + + # Simulate for a few steps + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + sim.step() + # Simulate physics + for _ in range(10): + # perform rendering + sim.step() + # update camera + cam_1.update(dt) + cam_2.update(dt) + # check image data + for cam in [cam_1, cam_2]: + for im_data in cam.data.output.values(): + assert im_data.shape == (1, camera_cfg.pattern_cfg.height, camera_cfg.pattern_cfg.width, 1) + + del cam_1, cam_2 + + +@pytest.mark.isaacsim_ci +def test_camera_set_world_poses(setup_simulation): + """Test camera function to set specific world pose.""" + sim, dt, camera_cfg = setup_simulation + + camera = MultiMeshRayCasterCamera(camera_cfg) + # play sim + sim.reset() + + # convert to torch tensors + position = torch.tensor([POSITION], dtype=torch.float32, device=camera.device) + orientation = torch.tensor([QUAT_WORLD], dtype=torch.float32, device=camera.device) + # set new pose + camera.set_world_poses(position.clone(), orientation.clone(), convention="world") + + # check if transform correctly set in output + torch.testing.assert_close(camera.data.pos_w, position) + torch.testing.assert_close(camera.data.quat_w_world, orientation) + + del camera + + +@pytest.mark.isaacsim_ci +def test_camera_set_world_poses_from_view(setup_simulation): + """Test camera function to set specific world pose from view.""" + sim, dt, camera_cfg = setup_simulation + + camera = MultiMeshRayCasterCamera(camera_cfg) + # play sim + sim.reset() + + # convert to torch tensors + eyes = torch.tensor([POSITION], dtype=torch.float32, device=camera.device) + targets = torch.tensor([[0.0, 0.0, 0.0]], dtype=torch.float32, device=camera.device) + quat_ros_gt = torch.tensor([QUAT_ROS], dtype=torch.float32, device=camera.device) + # set new pose + camera.set_world_poses_from_view(eyes.clone(), targets.clone()) + + # check if transform correctly set in output + torch.testing.assert_close(camera.data.pos_w, eyes) + torch.testing.assert_close(camera.data.quat_w_ros, quat_ros_gt) + + del camera + + +@pytest.mark.parametrize("height,width", [(240, 320), (480, 640)]) +@pytest.mark.isaacsim_ci +def test_intrinsic_matrix(setup_simulation, height, width): + """Checks that the camera's set and retrieve methods work for intrinsic matrix.""" + sim, dt, camera_cfg = setup_simulation + + camera_cfg_copy = copy.deepcopy(camera_cfg) + camera_cfg_copy.pattern_cfg.height = height + camera_cfg_copy.pattern_cfg.width = width + camera = MultiMeshRayCasterCamera(camera_cfg_copy) + # play sim + sim.reset() + # Desired properties (obtained from realsense camera at 320x240 resolution) + rs_intrinsic_matrix = [229.31640625, 0.0, 164.810546875, 0.0, 229.826171875, 122.1650390625, 0.0, 0.0, 1.0] + rs_intrinsic_matrix = torch.tensor(rs_intrinsic_matrix, device=camera.device).reshape(3, 3).unsqueeze(0) + # Set matrix into simulator + camera.set_intrinsic_matrices(rs_intrinsic_matrix.clone()) + # Simulate for a few steps + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + sim.step() + # Simulate physics + for _ in range(10): + # perform rendering + sim.step() + # update camera + camera.update(dt) + # Check that matrix is correct + torch.testing.assert_close(rs_intrinsic_matrix, camera.data.intrinsic_matrices) + + del camera + + +@pytest.mark.isaacsim_ci +def test_throughput(setup_simulation): + """Test camera throughput for different image sizes.""" + sim, dt, camera_cfg = setup_simulation + + # Create directory temp dir to dump the results + file_dir = os.path.dirname(os.path.realpath(__file__)) + temp_dir = os.path.join(file_dir, "output", "camera", "throughput") + os.makedirs(temp_dir, exist_ok=True) + # Create replicator writer + rep_writer = rep.BasicWriter(output_dir=temp_dir, frame_padding=3) + # create camera + camera_cfg_copy = copy.deepcopy(camera_cfg) + camera_cfg_copy.pattern_cfg.height = 480 + camera_cfg_copy.pattern_cfg.width = 640 + camera = MultiMeshRayCasterCamera(camera_cfg_copy) + + # Play simulator + sim.reset() + + # Set camera pose + eyes = torch.tensor([[2.5, 2.5, 2.5]], dtype=torch.float32, device=camera.device) + targets = torch.tensor([[0.0, 0.0, 0.0]], dtype=torch.float32, device=camera.device) + camera.set_world_poses_from_view(eyes, targets) + + # Simulate for a few steps + # note: This is a workaround to ensure that the textures are loaded. + # Check "Known Issues" section in the documentation for more details. + for _ in range(5): + sim.step() + # Simulate physics + for _ in range(5): + # perform rendering + sim.step() + # update camera + with Timer(f"Time taken for updating camera with shape {camera.image_shape}"): + camera.update(dt) + # Save images + with Timer(f"Time taken for writing data with shape {camera.image_shape} "): + # Pack data back into replicator format to save them using its writer + rep_output = {"annotators": {}} + camera_data = convert_dict_to_backend(camera.data.output, backend="numpy") + for key, data, info in zip(camera_data.keys(), camera_data.values(), camera.data.info[0].values()): + if info is not None: + rep_output["annotators"][key] = {"render_product": {"data": data, **info}} + else: + rep_output["annotators"][key] = {"render_product": {"data": data}} + # Save images + rep_output["trigger_outputs"] = {"on_time": camera.frame[0]} + rep_writer.write(rep_output) + print("----------------------------------------") + # Check image data + for im_data in camera.data.output.values(): + assert im_data.shape == (1, camera_cfg_copy.pattern_cfg.height, camera_cfg_copy.pattern_cfg.width, 1) + + del camera + + +@pytest.mark.parametrize( + "data_types", + [ + ["distance_to_image_plane", "distance_to_camera", "normals"], + ["distance_to_image_plane"], + ["distance_to_camera"], + ], +) +@pytest.mark.isaacsim_ci +def test_output_equal_to_usdcamera(setup_simulation, data_types): + """Test that ray caster camera output equals USD camera output.""" + sim, dt, camera_cfg = setup_simulation + + camera_pattern_cfg = patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=240, + width=320, + ) + sim_utils.create_prim("/World/Camera_warp", "Xform") + camera_cfg_warp = MultiMeshRayCasterCameraCfg( + prim_path="/World/Camera_warp", + mesh_prim_paths=["/World/defaultGroundPlane"], + update_period=0, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + debug_vis=False, + pattern_cfg=camera_pattern_cfg, + data_types=data_types, + ) + + camera_warp = MultiMeshRayCasterCamera(camera_cfg_warp) + + # create usd camera + camera_cfg_usd = CameraCfg( + height=240, + width=320, + prim_path="/World/Camera_usd", + update_period=0, + data_types=data_types, + spawn=PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(1e-4, 1.0e5) + ), + ) + camera_usd = Camera(camera_cfg_usd) + + # play sim + sim.reset() + sim.play() + + # convert to torch tensors + eyes = torch.tensor([[2.5, 2.5, 4.5]], dtype=torch.float32, device=camera_warp.device) + targets = torch.tensor([[0.0, 0.0, 0.0]], dtype=torch.float32, device=camera_warp.device) + # set views + camera_warp.set_world_poses_from_view(eyes, targets) + camera_usd.set_world_poses_from_view(eyes, targets) + + # perform steps + for _ in range(5): + sim.step() + + # update camera + camera_usd.update(dt) + camera_warp.update(dt) + + # check the intrinsic matrices + torch.testing.assert_close( + camera_usd.data.intrinsic_matrices, + camera_warp.data.intrinsic_matrices, + ) + + # check the apertures + torch.testing.assert_close( + camera_usd._sensor_prims[0].GetHorizontalApertureAttr().Get(), + camera_cfg_warp.pattern_cfg.horizontal_aperture, + ) + + # check image data + for data_type in data_types: + if data_type in camera_usd.data.output and data_type in camera_warp.data.output: + if data_type == "distance_to_camera" or data_type == "distance_to_image_plane": + torch.testing.assert_close( + camera_usd.data.output[data_type], + camera_warp.data.output[data_type], + atol=5e-5, + rtol=5e-6, + ) + elif data_type == "normals": + # NOTE: floating point issues of ~1e-5, so using atol and rtol in this case + torch.testing.assert_close( + camera_usd.data.output[data_type][..., :3], + camera_warp.data.output[data_type], + rtol=1e-5, + atol=1e-4, + ) + else: + torch.testing.assert_close( + camera_usd.data.output[data_type], + camera_warp.data.output[data_type], + ) + + del camera_usd, camera_warp + + +@pytest.mark.isaacsim_ci +def test_output_equal_to_usdcamera_offset(setup_simulation): + """Test that ray caster camera output equals USD camera output with offset.""" + sim, dt, camera_cfg = setup_simulation + offset_rot = (-0.1251, 0.3617, 0.8731, -0.3020) + + camera_pattern_cfg = patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=240, + width=320, + ) + sim_utils.create_prim("/World/Camera_warp", "Xform") + camera_cfg_warp = MultiMeshRayCasterCameraCfg( + prim_path="/World/Camera_warp", + mesh_prim_paths=["/World/defaultGroundPlane"], + update_period=0, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(2.5, 2.5, 4.0), rot=offset_rot, convention="ros"), + debug_vis=False, + pattern_cfg=camera_pattern_cfg, + data_types=["distance_to_image_plane", "distance_to_camera", "normals"], + ) + camera_warp = MultiMeshRayCasterCamera(camera_cfg_warp) + + # create usd camera + camera_cfg_usd = CameraCfg( + height=240, + width=320, + prim_path="/World/Camera_usd", + update_period=0, + data_types=["distance_to_image_plane", "distance_to_camera", "normals"], + spawn=PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(1e-6, 1.0e5) + ), + offset=CameraCfg.OffsetCfg(pos=(2.5, 2.5, 4.0), rot=offset_rot, convention="ros"), + ) + camera_usd = Camera(camera_cfg_usd) + + # play sim + sim.reset() + sim.play() + + # perform steps + for _ in range(5): + sim.step() + + # update camera + camera_usd.update(dt) + camera_warp.update(dt) + + # check image data + torch.testing.assert_close( + camera_usd.data.output["distance_to_image_plane"], + camera_warp.data.output["distance_to_image_plane"], + atol=5e-5, + rtol=5e-6, + ) + torch.testing.assert_close( + camera_usd.data.output["distance_to_camera"], + camera_warp.data.output["distance_to_camera"], + atol=5e-5, + rtol=5e-6, + ) + + # check normals + # NOTE: floating point issues of ~1e-5, so using atol and rtol in this case + torch.testing.assert_close( + camera_usd.data.output["normals"][..., :3], + camera_warp.data.output["normals"], + rtol=1e-5, + atol=1e-4, + ) + + del camera_usd, camera_warp + + +@pytest.mark.isaacsim_ci +def test_output_equal_to_usdcamera_prim_offset(setup_simulation): + """Test that the output of the ray caster camera is equal to the output of the usd camera when both are placed + under an XForm prim that is translated and rotated from the world origin.""" + sim, dt, camera_cfg = setup_simulation + + offset_rot = [-0.1251, 0.3617, 0.8731, -0.3020] + + # gf quat + gf_quatf = Gf.Quatd() + gf_quatf.SetReal(QUAT_OPENGL[0]) + gf_quatf.SetImaginary(tuple(QUAT_OPENGL[1:])) + + camera_pattern_cfg = patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=240, + width=320, + ) + prim_raycast_cam = sim_utils.create_prim("/World/Camera_warp", "Xform") + prim_raycast_cam.GetAttribute("xformOp:translate").Set(tuple(POSITION)) + prim_raycast_cam.GetAttribute("xformOp:orient").Set(gf_quatf) + + camera_cfg_warp = MultiMeshRayCasterCameraCfg( + prim_path="/World/Camera_warp", + mesh_prim_paths=["/World/defaultGroundPlane"], + update_period=0, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(0, 0, 2.0), rot=offset_rot, convention="ros"), + debug_vis=False, + pattern_cfg=camera_pattern_cfg, + data_types=["distance_to_image_plane", "distance_to_camera", "normals"], + ) + + camera_warp = MultiMeshRayCasterCamera(camera_cfg_warp) + + # create usd camera + camera_cfg_usd = CameraCfg( + height=240, + width=320, + prim_path="/World/Camera_usd/camera", + update_period=0, + data_types=["distance_to_image_plane", "distance_to_camera", "normals"], + spawn=PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(1e-6, 1.0e5) + ), + offset=CameraCfg.OffsetCfg(pos=(0, 0, 2.0), rot=offset_rot, convention="ros"), + update_latest_camera_pose=True, + ) + prim_usd = sim_utils.create_prim("/World/Camera_usd", "Xform") + prim_usd.GetAttribute("xformOp:translate").Set(tuple(POSITION)) + prim_usd.GetAttribute("xformOp:orient").Set(gf_quatf) + + camera_usd = Camera(camera_cfg_usd) + + # play sim + sim.reset() + sim.play() + + # perform steps + for _ in range(5): + sim.step() + + # update camera + camera_usd.update(dt) + camera_warp.update(dt) + + # check if pos and orientation are correct + torch.testing.assert_close(camera_warp.data.pos_w[0], camera_usd.data.pos_w[0]) + torch.testing.assert_close(camera_warp.data.quat_w_ros[0], camera_usd.data.quat_w_ros[0]) + + # check image data + torch.testing.assert_close( + camera_usd.data.output["distance_to_image_plane"], + camera_warp.data.output["distance_to_image_plane"], + atol=5e-5, + rtol=5e-6, + ) + torch.testing.assert_close( + camera_usd.data.output["distance_to_camera"], + camera_warp.data.output["distance_to_camera"], + rtol=4e-6, + atol=2e-5, + ) + + # check normals + # NOTE: floating point issues of ~1e-5, so using atol and rtol in this case + torch.testing.assert_close( + camera_usd.data.output["normals"][..., :3], + camera_warp.data.output["normals"], + rtol=1e-5, + atol=1e-4, + ) + + del camera_usd, camera_warp + + +@pytest.mark.parametrize("height,width", [(540, 960), (240, 320)]) +@pytest.mark.isaacsim_ci +def test_output_equal_to_usd_camera_intrinsics(setup_simulation, height, width): + """Test that the output of the ray caster camera and usd camera are the same when both are + initialized with the same intrinsic matrix.""" + sim, dt, camera_cfg = setup_simulation + + # create cameras + offset_rot = [-0.1251, 0.3617, 0.8731, -0.3020] + offset_pos = (2.5, 2.5, 4.0) + intrinsics = [380.0831, 0.0, width / 2, 0.0, 380.0831, height / 2, 0.0, 0.0, 1.0] + sim_utils.create_prim("/World/Camera_warp", "Xform") + # get camera cfgs + camera_warp_cfg = MultiMeshRayCasterCameraCfg( + prim_path="/World/Camera_warp", + mesh_prim_paths=["/World/defaultGroundPlane"], + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=offset_pos, rot=offset_rot, convention="ros"), + debug_vis=False, + pattern_cfg=patterns.PinholeCameraPatternCfg.from_intrinsic_matrix( + intrinsic_matrix=intrinsics, + height=height, + width=width, + focal_length=38.0, + ), + max_distance=25.0, + data_types=["distance_to_image_plane"], + ) + camera_usd_cfg = CameraCfg( + prim_path="/World/Camera_usd", + offset=CameraCfg.OffsetCfg(pos=offset_pos, rot=offset_rot, convention="ros"), + spawn=PinholeCameraCfg.from_intrinsic_matrix( + intrinsic_matrix=intrinsics, + height=height, + width=width, + clipping_range=(0.01, 25), + focal_length=38.0, + ), + height=height, + width=width, + data_types=["distance_to_image_plane"], + ) + + # set aperture offsets to 0, as currently not supported for usd camera + camera_warp_cfg.pattern_cfg.horizontal_aperture_offset = 0 + camera_warp_cfg.pattern_cfg.vertical_aperture_offset = 0 + camera_usd_cfg.spawn.horizontal_aperture_offset = 0 + camera_usd_cfg.spawn.vertical_aperture_offset = 0 + # init cameras + camera_warp = MultiMeshRayCasterCamera(camera_warp_cfg) + camera_usd = Camera(camera_usd_cfg) + + # play sim + sim.reset() + sim.play() + + # perform steps + for _ in range(5): + sim.step() + + # update camera + camera_usd.update(dt) + camera_warp.update(dt) + + # filter nan and inf from output + cam_warp_output = camera_warp.data.output["distance_to_image_plane"].clone() + cam_usd_output = camera_usd.data.output["distance_to_image_plane"].clone() + cam_warp_output[torch.isnan(cam_warp_output)] = 0 + cam_warp_output[torch.isinf(cam_warp_output)] = 0 + cam_usd_output[torch.isnan(cam_usd_output)] = 0 + cam_usd_output[torch.isinf(cam_usd_output)] = 0 + + # check that both have the same intrinsic matrices + torch.testing.assert_close(camera_warp.data.intrinsic_matrices[0], camera_usd.data.intrinsic_matrices[0]) + + # check the apertures + torch.testing.assert_close( + camera_usd._sensor_prims[0].GetHorizontalApertureAttr().Get(), + camera_warp_cfg.pattern_cfg.horizontal_aperture, + ) + torch.testing.assert_close( + camera_usd._sensor_prims[0].GetVerticalApertureAttr().Get(), + camera_warp_cfg.pattern_cfg.vertical_aperture, + ) + + # check image data + torch.testing.assert_close( + cam_warp_output, + cam_usd_output, + atol=5e-5, + rtol=5e-6, + ) + + del camera_usd, camera_warp + + +@pytest.mark.isaacsim_ci +def test_output_equal_to_usd_camera_when_intrinsics_set(setup_simulation): + """Test that the output of the ray caster camera is equal to the output of the usd camera when both are placed + under an XForm prim and an intrinsic matrix is set.""" + sim, dt, camera_cfg = setup_simulation + + camera_pattern_cfg = patterns.PinholeCameraPatternCfg( + focal_length=24.0, + horizontal_aperture=20.955, + height=540, + width=960, + ) + camera_cfg_warp = MultiMeshRayCasterCameraCfg( + prim_path="/World/Camera", + mesh_prim_paths=["/World/defaultGroundPlane"], + update_period=0, + offset=MultiMeshRayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + debug_vis=False, + pattern_cfg=camera_pattern_cfg, + data_types=["distance_to_camera"], + ) + + camera_warp = MultiMeshRayCasterCamera(camera_cfg_warp) + + # create usd camera + camera_cfg_usd = CameraCfg( + height=540, + width=960, + prim_path="/World/Camera_usd", + update_period=0, + data_types=["distance_to_camera"], + spawn=PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(1e-4, 1.0e5) + ), + ) + camera_usd = Camera(camera_cfg_usd) + + # play sim + sim.reset() + sim.play() + + # set intrinsic matrix + # NOTE: extend the test to cover aperture offsets once supported by the usd camera + intrinsic_matrix = torch.tensor( + [[380.0831, 0.0, camera_cfg_usd.width / 2, 0.0, 380.0831, camera_cfg_usd.height / 2, 0.0, 0.0, 1.0]], + device=camera_warp.device, + ).reshape(1, 3, 3) + camera_warp.set_intrinsic_matrices(intrinsic_matrix, focal_length=10) + camera_usd.set_intrinsic_matrices(intrinsic_matrix, focal_length=10) + + # set camera position + camera_warp.set_world_poses_from_view( + eyes=torch.tensor([[0.0, 0.0, 5.0]], device=camera_warp.device), + targets=torch.tensor([[0.0, 0.0, 0.0]], device=camera_warp.device), + ) + camera_usd.set_world_poses_from_view( + eyes=torch.tensor([[0.0, 0.0, 5.0]], device=camera_usd.device), + targets=torch.tensor([[0.0, 0.0, 0.0]], device=camera_usd.device), + ) + + # perform steps + for _ in range(5): + sim.step() + + # update camera + camera_usd.update(dt) + camera_warp.update(dt) + + # check image data + torch.testing.assert_close( + camera_usd.data.output["distance_to_camera"], + camera_warp.data.output["distance_to_camera"], + rtol=5e-3, + atol=1e-4, + ) + + del camera_usd, camera_warp diff --git a/source/isaaclab/test/sensors/test_multi_tiled_camera.py b/source/isaaclab/test/sensors/test_multi_tiled_camera.py index 7408ae06b75b..a1fb9a178351 100644 --- a/source/isaaclab/test/sensors/test_multi_tiled_camera.py +++ b/source/isaaclab/test/sensors/test_multi_tiled_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,15 +16,14 @@ """Rest everything follows.""" import copy -import numpy as np import random -import torch -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils -import omni.replicator.core as rep +import numpy as np import pytest +import torch from flaky import flaky + +import omni.replicator.core as rep from isaacsim.core.prims import SingleGeometryPrim, SingleRigidPrim from pxr import Gf, UsdGeom @@ -47,7 +46,7 @@ def setup_camera(): ), ) # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.01 # Load kit helper @@ -56,7 +55,7 @@ def setup_camera(): # populate scene _populate_scene() # load stage - stage_utils.update_stage() + sim_utils.update_stage() yield camera_cfg, sim, dt # Teardown rep.vp_manager.destroy_hydra_textures("Replicator") @@ -78,7 +77,7 @@ def test_multi_tiled_camera_init(setup_camera): tiled_cameras = [] for i in range(num_tiled_cameras): for j in range(num_cameras_per_tiled_camera): - prim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -173,7 +172,7 @@ def test_all_annotators_multi_tiled_camera(setup_camera): tiled_cameras = [] for i in range(num_tiled_cameras): for j in range(num_cameras_per_tiled_camera): - prim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -274,7 +273,7 @@ def test_different_resolution_multi_tiled_camera(setup_camera): resolutions = [(16, 16), (23, 765)] for i in range(num_tiled_cameras): for j in range(num_cameras_per_tiled_camera): - prim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -345,7 +344,7 @@ def test_frame_offset_multi_tiled_camera(setup_camera): tiled_cameras = [] for i in range(num_tiled_cameras): for j in range(num_cameras_per_tiled_camera): - prim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -354,7 +353,7 @@ def test_frame_offset_multi_tiled_camera(setup_camera): tiled_cameras.append(camera) # modify scene to be less stochastic - stage = stage_utils.get_current_stage() + stage = sim_utils.get_current_stage() for i in range(10): prim = stage.GetPrimAtPath(f"/World/Objects/Obj_{i:02d}") color = Gf.Vec3f(1, 1, 1) @@ -413,7 +412,7 @@ def test_frame_different_poses_multi_tiled_camera(setup_camera): tiled_cameras = [] for i in range(num_tiled_cameras): for j in range(num_cameras_per_tiled_camera): - prim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}_{j}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -491,7 +490,7 @@ def _populate_scene(): position *= np.asarray([1.5, 1.5, 0.5]) # create prim prim_type = random.choice(["Cube", "Sphere", "Cylinder"]) - prim = prim_utils.create_prim( + prim = sim_utils.create_prim( f"/World/Objects/Obj_{i:02d}", prim_type, translation=position, diff --git a/source/isaaclab/test/sensors/test_outdated_sensor.py b/source/isaaclab/test/sensors/test_outdated_sensor.py index e4de65b50004..ac0c989c6839 100644 --- a/source/isaaclab/test/sensors/test_outdated_sensor.py +++ b/source/isaaclab/test/sensors/test_outdated_sensor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,14 +12,15 @@ """Rest everything follows.""" -import gymnasium as gym import shutil import tempfile + +import gymnasium as gym +import pytest import torch import carb import omni.usd -import pytest import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.parse_cfg import parse_env_cfg diff --git a/source/isaaclab/test/sensors/test_ray_caster.py b/source/isaaclab/test/sensors/test_ray_caster.py new file mode 100644 index 000000000000..01b2dde1ae2a --- /dev/null +++ b/source/isaaclab/test/sensors/test_ray_caster.py @@ -0,0 +1,241 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import numpy as np +import pytest +import torch +import trimesh + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True, enable_cameras=True).app + +# Import after app launch +import warp as wp + +from isaaclab.utils.math import matrix_from_quat, quat_from_euler_xyz, random_orientation +from isaaclab.utils.warp.ops import convert_to_warp_mesh, raycast_dynamic_meshes, raycast_mesh + + +@pytest.fixture(scope="module") +def raycast_setup(): + device = "cuda" if torch.cuda.is_available() else "cpu" + # Base trimesh cube and its Warp conversion + trimesh_mesh = trimesh.creation.box([2, 2, 1]) + single_mesh = [ + convert_to_warp_mesh( + trimesh_mesh.vertices, + trimesh_mesh.faces, + device, + ) + ] + single_mesh_id = single_mesh[0].id + + # Rays + ray_starts = torch.tensor([[0, -0.35, -5], [0.25, 0.35, -5]], dtype=torch.float32, device=device).unsqueeze(0) + ray_directions = torch.tensor([[0, 0, 1], [0, 0, 1]], dtype=torch.float32, device=device).unsqueeze(0) + expected_ray_hits = torch.tensor( + [[0, -0.35, -0.5], [0.25, 0.35, -0.5]], dtype=torch.float32, device=device + ).unsqueeze(0) + + return { + "device": device, + "trimesh_mesh": trimesh_mesh, + "single_mesh_id": single_mesh_id, + "wp_mesh": single_mesh[0], + "ray_starts": ray_starts, + "ray_directions": ray_directions, + "expected_ray_hits": expected_ray_hits, + } + + +def test_raycast_multi_cubes(raycast_setup): + device = raycast_setup["device"] + base_tm = raycast_setup["trimesh_mesh"] + + tm1 = base_tm.copy() + wp_mesh_1 = convert_to_warp_mesh(tm1.vertices, tm1.faces, device) + + translation = np.eye(4) + translation[:3, 3] = [0, 2, 0] + tm2 = base_tm.copy().apply_transform(translation) + wp_mesh_2 = convert_to_warp_mesh(tm2.vertices, tm2.faces, device) + + mesh_ids_wp = wp.array2d([[wp_mesh_1.id, wp_mesh_2.id]], dtype=wp.uint64, device=device) + + ray_directions = raycast_setup["ray_directions"] + + # Case 1 + ray_start = torch.tensor([[0, 0, -5], [0, 2.5, -5]], dtype=torch.float32, device=device).unsqueeze(0) + ray_hits, ray_distance, ray_normal, ray_face_id, mesh_ids = raycast_dynamic_meshes( + ray_start, + ray_directions, + mesh_ids_wp, + return_distance=True, + return_normal=True, + return_face_id=True, + return_mesh_id=True, + ) + + torch.testing.assert_close(ray_hits, torch.tensor([[[0, 0, -0.5], [0, 2.5, -0.5]]], device=device)) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], device=device, dtype=torch.float32)) + assert torch.equal(mesh_ids, torch.tensor([[0, 1]], dtype=torch.int32, device=device)) + + # Case 2 (explicit poses/orientations) + ray_start = torch.tensor([[0, 0, -5], [0, 4.5, -5]], dtype=torch.float32, device=device).unsqueeze(0) + ray_hits, ray_distance, ray_normal, ray_face_id, mesh_ids = raycast_dynamic_meshes( + ray_start, + ray_directions, + mesh_ids_wp, + return_distance=True, + return_normal=True, + return_face_id=True, + mesh_positions_w=torch.tensor([[[0, 0, 0], [0, 2, 0]]], dtype=torch.float32, device=device), + mesh_orientations_w=torch.tensor([[[1, 0, 0, 0], [1, 0, 0, 0]]], dtype=torch.float32, device=device), + return_mesh_id=True, + ) + + torch.testing.assert_close(ray_hits, torch.tensor([[[0, 0, -0.5], [0, 4.5, -0.5]]], device=device)) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], device=device, dtype=torch.float32)) + assert torch.equal(mesh_ids, torch.tensor([[0, 1]], dtype=torch.int32, device=device)) + + +def test_raycast_single_cube(raycast_setup): + device = raycast_setup["device"] + ray_starts = raycast_setup["ray_starts"] + ray_directions = raycast_setup["ray_directions"] + mesh = raycast_setup["wp_mesh"] + expected_ray_hits = raycast_setup["expected_ray_hits"] + single_mesh_id = raycast_setup["single_mesh_id"] + + # Single-mesh helper + ray_hits, ray_distance, ray_normal, ray_face_id = raycast_mesh( + ray_starts, + ray_directions, + mesh, + return_distance=True, + return_normal=True, + return_face_id=True, + ) + torch.testing.assert_close(ray_hits, expected_ray_hits) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], device=device, dtype=torch.float32)) + torch.testing.assert_close(ray_face_id, torch.tensor([[3, 8]], dtype=torch.int32, device=device)) + + # Multi-mesh API with one mesh + ray_hits, ray_distance, ray_normal, ray_face_id, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + ) + torch.testing.assert_close(ray_hits, expected_ray_hits) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], device=device, dtype=torch.float32)) + torch.testing.assert_close(ray_face_id, torch.tensor([[3, 8]], dtype=torch.int32, device=device)) + + +def test_raycast_moving_cube(raycast_setup): + device = raycast_setup["device"] + ray_starts = raycast_setup["ray_starts"] + ray_directions = raycast_setup["ray_directions"] + single_mesh_id = raycast_setup["single_mesh_id"] + expected_ray_hits = raycast_setup["expected_ray_hits"] + + for distance in torch.linspace(0, 1, 10, device=device): + ray_hits, ray_distance, ray_normal, ray_face_id, mesh_id = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + return_mesh_id=True, + mesh_positions_w=torch.tensor([[0, 0, distance.item()]], dtype=torch.float32, device=device), + ) + offset = torch.tensor([[0, 0, distance.item()], [0, 0, distance.item()]], dtype=torch.float32, device=device) + torch.testing.assert_close(ray_hits, expected_ray_hits + offset.unsqueeze(0)) + torch.testing.assert_close(ray_distance, distance + torch.tensor([[4.5, 4.5]], device=device)) + torch.testing.assert_close( + ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], device=device, dtype=torch.float32) + ) + torch.testing.assert_close(ray_face_id, torch.tensor([[3, 8]], dtype=torch.int32, device=device)) + + +def test_raycast_rotated_cube(raycast_setup): + device = raycast_setup["device"] + ray_starts = raycast_setup["ray_starts"] + ray_directions = raycast_setup["ray_directions"] + single_mesh_id = raycast_setup["single_mesh_id"] + expected_ray_hits = raycast_setup["expected_ray_hits"] + + cube_rotation = quat_from_euler_xyz( + torch.tensor([0.0], device=device), torch.tensor([0.0], device=device), torch.tensor([np.pi], device=device) + ) + ray_hits, ray_distance, ray_normal, ray_face_id, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + mesh_orientations_w=cube_rotation.unsqueeze(0), + ) + torch.testing.assert_close(ray_hits, expected_ray_hits) + torch.testing.assert_close(ray_distance, torch.tensor([[4.5, 4.5]], device=device)) + torch.testing.assert_close(ray_normal, torch.tensor([[[0, 0, -1], [0, 0, -1]]], device=device, dtype=torch.float32)) + # Rotated cube swaps face IDs + torch.testing.assert_close(ray_face_id, torch.tensor([[8, 3]], dtype=torch.int32, device=device)) + + +def test_raycast_random_cube(raycast_setup): + device = raycast_setup["device"] + base_tm = raycast_setup["trimesh_mesh"] + ray_starts = raycast_setup["ray_starts"] + ray_directions = raycast_setup["ray_directions"] + single_mesh_id = raycast_setup["single_mesh_id"] + + for orientation in random_orientation(10, device): + pos = torch.tensor([[0.0, 0.0, torch.rand(1, device=device).item()]], dtype=torch.float32, device=device) + + tf_hom = np.eye(4) + tf_hom[:3, :3] = matrix_from_quat(orientation).cpu().numpy() + tf_hom[:3, 3] = pos.squeeze(0).cpu().numpy() + + tf_mesh = base_tm.copy().apply_transform(tf_hom) + wp_mesh = convert_to_warp_mesh(tf_mesh.vertices, tf_mesh.faces, device) + + # Raycast transformed, static mesh + ray_hits, ray_distance, ray_normal, ray_face_id, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[wp_mesh.id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + ) + # Raycast original mesh with pose provided + ray_hits_m, ray_distance_m, ray_normal_m, ray_face_id_m, _ = raycast_dynamic_meshes( + ray_starts, + ray_directions, + wp.array2d([[single_mesh_id]], dtype=wp.uint64, device=device), + return_distance=True, + return_normal=True, + return_face_id=True, + mesh_positions_w=pos, + mesh_orientations_w=orientation.view(1, 1, -1), + ) + + torch.testing.assert_close(ray_hits, ray_hits_m) + torch.testing.assert_close(ray_distance, ray_distance_m) + torch.testing.assert_close(ray_normal, ray_normal_m) + torch.testing.assert_close(ray_face_id, ray_face_id_m) diff --git a/source/isaaclab/test/sensors/test_ray_caster_camera.py b/source/isaaclab/test/sensors/test_ray_caster_camera.py index dd693bbc3f12..8b4c2f4a973a 100644 --- a/source/isaaclab/test/sensors/test_ray_caster_camera.py +++ b/source/isaaclab/test/sensors/test_ray_caster_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,14 +16,13 @@ """Rest everything follows.""" import copy -import numpy as np import os + +import numpy as np +import pytest import torch -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import omni.replicator.core as rep -import pytest from pxr import Gf import isaaclab.sim as sim_utils @@ -64,9 +63,9 @@ def setup() -> tuple[sim_utils.SimulationContext, RayCasterCameraCfg, float]: ], ) # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # create xform because placement of camera directly under world is not supported - prim_utils.create_prim("/World/Camera", "Xform") + sim_utils.create_prim("/World/Camera", "Xform") # Simulation time-step dt = 0.01 # Load kit helper @@ -76,7 +75,7 @@ def setup() -> tuple[sim_utils.SimulationContext, RayCasterCameraCfg, float]: mesh = make_plane(size=(100, 100), height=0.0, center_zero=True) create_prim_from_mesh("/World/defaultGroundPlane", mesh) # load stage - stage_utils.update_stage() + sim_utils.update_stage() return sim, camera_cfg, dt @@ -160,9 +159,9 @@ def test_depth_clipping(setup_sim): This test is the same for all camera models to enforce the same clipping behavior. """ sim, camera_cfg, dt = setup_sim - prim_utils.create_prim("/World/CameraZero", "Xform") - prim_utils.create_prim("/World/CameraNone", "Xform") - prim_utils.create_prim("/World/CameraMax", "Xform") + sim_utils.create_prim("/World/CameraZero", "Xform") + sim_utils.create_prim("/World/CameraNone", "Xform") + sim_utils.create_prim("/World/CameraMax", "Xform") # get camera cfgs camera_cfg_zero = RayCasterCameraCfg( @@ -252,7 +251,7 @@ def test_camera_init_offset(setup_sim): rot=(QUAT_ROS[0], QUAT_ROS[1], QUAT_ROS[2], QUAT_ROS[3]), convention="ros", ) - prim_utils.create_prim("/World/CameraOffsetRos", "Xform") + sim_utils.create_prim("/World/CameraOffsetRos", "Xform") cam_cfg_offset_ros.prim_path = "/World/CameraOffsetRos" camera_ros = RayCasterCamera(cam_cfg_offset_ros) # -- OpenGL convention @@ -262,7 +261,7 @@ def test_camera_init_offset(setup_sim): rot=(QUAT_OPENGL[0], QUAT_OPENGL[1], QUAT_OPENGL[2], QUAT_OPENGL[3]), convention="opengl", ) - prim_utils.create_prim("/World/CameraOffsetOpengl", "Xform") + sim_utils.create_prim("/World/CameraOffsetOpengl", "Xform") cam_cfg_offset_opengl.prim_path = "/World/CameraOffsetOpengl" camera_opengl = RayCasterCamera(cam_cfg_offset_opengl) # -- World convention @@ -272,7 +271,7 @@ def test_camera_init_offset(setup_sim): rot=(QUAT_WORLD[0], QUAT_WORLD[1], QUAT_WORLD[2], QUAT_WORLD[3]), convention="world", ) - prim_utils.create_prim("/World/CameraOffsetWorld", "Xform") + sim_utils.create_prim("/World/CameraOffsetWorld", "Xform") cam_cfg_offset_world.prim_path = "/World/CameraOffsetWorld" camera_world = RayCasterCamera(cam_cfg_offset_world) @@ -356,13 +355,13 @@ def test_multi_camera_init(setup_sim): # -- camera 1 cam_cfg_1 = copy.deepcopy(camera_cfg) cam_cfg_1.prim_path = "/World/Camera_1" - prim_utils.create_prim("/World/Camera_1", "Xform") + sim_utils.create_prim("/World/Camera_1", "Xform") # Create camera cam_1 = RayCasterCamera(cam_cfg_1) # -- camera 2 cam_cfg_2 = copy.deepcopy(camera_cfg) cam_cfg_2.prim_path = "/World/Camera_2" - prim_utils.create_prim("/World/Camera_2", "Xform") + sim_utils.create_prim("/World/Camera_2", "Xform") cam_2 = RayCasterCamera(cam_cfg_2) # check that the loaded meshes are equal @@ -512,7 +511,7 @@ def test_output_equal_to_usdcamera(setup_sim): height=240, width=320, ) - prim_utils.create_prim("/World/Camera_warp", "Xform") + sim_utils.create_prim("/World/Camera_warp", "Xform") camera_cfg_warp = RayCasterCameraCfg( prim_path="/World/Camera", mesh_prim_paths=["/World/defaultGroundPlane"], @@ -611,7 +610,7 @@ def test_output_equal_to_usdcamera_offset(setup_sim): height=240, width=320, ) - prim_utils.create_prim("/World/Camera_warp", "Xform") + sim_utils.create_prim("/World/Camera_warp", "Xform") camera_cfg_warp = RayCasterCameraCfg( prim_path="/World/Camera", mesh_prim_paths=["/World/defaultGroundPlane"], @@ -695,7 +694,7 @@ def test_output_equal_to_usdcamera_prim_offset(setup_sim): height=240, width=320, ) - prim_raycast_cam = prim_utils.create_prim("/World/Camera_warp", "Xform") + prim_raycast_cam = sim_utils.create_prim("/World/Camera_warp", "Xform") prim_raycast_cam.GetAttribute("xformOp:translate").Set(tuple(POSITION)) prim_raycast_cam.GetAttribute("xformOp:orient").Set(gf_quatf) @@ -724,7 +723,7 @@ def test_output_equal_to_usdcamera_prim_offset(setup_sim): offset=CameraCfg.OffsetCfg(pos=(0, 0, 2.0), rot=offset_rot, convention="ros"), update_latest_camera_pose=True, ) - prim_usd = prim_utils.create_prim("/World/Camera_usd", "Xform") + prim_usd = sim_utils.create_prim("/World/Camera_usd", "Xform") prim_usd.GetAttribute("xformOp:translate").Set(tuple(POSITION)) prim_usd.GetAttribute("xformOp:orient").Set(gf_quatf) @@ -783,7 +782,7 @@ def test_output_equal_to_usd_camera_intrinsics(setup_sim, focal_length): offset_rot = (-0.1251, 0.3617, 0.8731, -0.3020) offset_pos = (2.5, 2.5, 4.0) intrinsics = [380.0831, 0.0, 480.0, 0.0, 380.0831, 270.0, 0.0, 0.0, 1.0] - prim_utils.create_prim("/World/Camera_warp", "Xform") + sim_utils.create_prim("/World/Camera_warp", "Xform") # get camera cfgs camera_warp_cfg = RayCasterCameraCfg( prim_path="/World/Camera_warp", diff --git a/source/isaaclab/test/sensors/test_ray_caster_patterns.py b/source/isaaclab/test/sensors/test_ray_caster_patterns.py new file mode 100644 index 000000000000..cab9e4af7245 --- /dev/null +++ b/source/isaaclab/test/sensors/test_ray_caster_patterns.py @@ -0,0 +1,426 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import math + +import pytest +import torch + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True, enable_cameras=False).app + +# Import after app launch +from isaaclab.sensors.ray_caster.patterns import patterns, patterns_cfg + + +@pytest.fixture(scope="module", params=["cuda", "cpu"]) +def device(request): + """Fixture to parameterize tests over both CUDA and CPU devices.""" + if request.param == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + return request.param + + +class TestGridPattern: + """Test cases for grid_pattern function.""" + + @pytest.mark.parametrize( + "size,resolution,ordering,expected_num_rays", + [ + ((2.0, 2.0), 1.0, "xy", 9), # 3x3 grid + ((2.0, 2.0), 0.5, "xy", 25), # 5x5 grid + ((4.0, 2.0), 1.0, "xy", 15), # 5x3 grid + ((2.0, 4.0), 1.0, "yx", 15), # 3x5 grid + ((1.0, 1.0), 0.25, "xy", 25), # 5x5 grid with smaller size + ], + ) + def test_grid_pattern_num_rays(self, device, size, resolution, ordering, expected_num_rays): + """Test that grid pattern generates the correct number of rays.""" + cfg = patterns_cfg.GridPatternCfg(size=size, resolution=resolution, ordering=ordering) + ray_starts, ray_directions = patterns.grid_pattern(cfg, device) + + assert ray_starts.shape[0] == expected_num_rays + assert ray_directions.shape[0] == expected_num_rays + assert ray_starts.shape[1] == 3 + assert ray_directions.shape[1] == 3 + + @pytest.mark.parametrize("ordering", ["xy", "yx"]) + def test_grid_pattern_ordering(self, device, ordering): + """Test that grid pattern respects the ordering parameter.""" + cfg = patterns_cfg.GridPatternCfg(size=(2.0, 2.0), resolution=1.0, ordering=ordering) + ray_starts, ray_directions = patterns.grid_pattern(cfg, device) + + # Check that the rays are ordered correctly + if ordering == "xy": + # For "xy" ordering, x should change faster than y + # First few rays should have same y, different x + assert ray_starts[0, 1] == ray_starts[1, 1] # Same y + assert ray_starts[0, 0] != ray_starts[1, 0] # Different x + else: # "yx" + # For "yx" ordering, y should change faster than x + # First few rays should have same x, different y + assert ray_starts[0, 0] == ray_starts[1, 0] # Same x + assert ray_starts[0, 1] != ray_starts[1, 1] # Different y + + @pytest.mark.parametrize("direction", [(0.0, 0.0, -1.0), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0)]) + def test_grid_pattern_direction(self, device, direction): + """Test that grid pattern uses the specified direction.""" + cfg = patterns_cfg.GridPatternCfg(size=(2.0, 2.0), resolution=1.0, direction=direction) + ray_starts, ray_directions = patterns.grid_pattern(cfg, device) + + expected_direction = torch.tensor(direction, device=device) + # All rays should have the same direction - check in batch + expected_directions = expected_direction.unsqueeze(0).expand_as(ray_directions) + torch.testing.assert_close(ray_directions, expected_directions) + + def test_grid_pattern_bounds(self, device): + """Test that grid pattern respects the size bounds.""" + size = (2.0, 4.0) + cfg = patterns_cfg.GridPatternCfg(size=size, resolution=1.0) + ray_starts, ray_directions = patterns.grid_pattern(cfg, device) + + # Check that all rays are within bounds + assert ray_starts[:, 0].min() >= -size[0] / 2 + assert ray_starts[:, 0].max() <= size[0] / 2 + assert ray_starts[:, 1].min() >= -size[1] / 2 + assert ray_starts[:, 1].max() <= size[1] / 2 + # Z should be 0 for grid pattern + torch.testing.assert_close(ray_starts[:, 2], torch.zeros_like(ray_starts[:, 2])) + + def test_grid_pattern_invalid_ordering(self, device): + """Test that invalid ordering raises ValueError.""" + cfg = patterns_cfg.GridPatternCfg(size=(2.0, 2.0), resolution=1.0, ordering="invalid") + with pytest.raises(ValueError, match="Ordering must be 'xy' or 'yx'"): + patterns.grid_pattern(cfg, device) + + def test_grid_pattern_invalid_resolution(self, device): + """Test that invalid resolution raises ValueError.""" + cfg = patterns_cfg.GridPatternCfg(size=(2.0, 2.0), resolution=-1.0) + with pytest.raises(ValueError, match="Resolution must be greater than 0"): + patterns.grid_pattern(cfg, device) + + +class TestLidarPattern: + """Test cases for lidar_pattern function.""" + + @pytest.mark.parametrize( + "horizontal_fov_range,horizontal_res,channels,vertical_fov_range", + [ + # Test 360 degree horizontal FOV + ((-180.0, 180.0), 90.0, 1, (-10.0, -10.0)), + ((-180.0, 180.0), 45.0, 1, (-10.0, -10.0)), + ((-180.0, 180.0), 1.0, 1, (-10.0, -10.0)), + # Test partial horizontal FOV + ((-90.0, 90.0), 30.0, 1, (-10.0, -10.0)), + ((0.0, 180.0), 45.0, 1, (-10.0, -10.0)), + # Test 360 no overlap case + ((-180.0, 180.0), 90.0, 1, (0.0, 0.0)), + # Test partial FOV case + ((-90.0, 90.0), 90.0, 1, (0.0, 0.0)), + # Test multiple channels + ((-180.0, 180.0), 90.0, 16, (-15.0, 15.0)), + ((-180.0, 180.0), 45.0, 32, (-30.0, 10.0)), + # Test single channel, different vertical angles + ((-180.0, 180.0), 90.0, 1, (45.0, 45.0)), + ], + ) + def test_lidar_pattern_num_rays(self, device, horizontal_fov_range, horizontal_res, channels, vertical_fov_range): + """Test that lidar pattern generates the correct number of rays.""" + cfg = patterns_cfg.LidarPatternCfg( + horizontal_fov_range=horizontal_fov_range, + horizontal_res=horizontal_res, + channels=channels, + vertical_fov_range=vertical_fov_range, + ) + ray_starts, ray_directions = patterns.lidar_pattern(cfg, device) + + # Calculate expected number of horizontal angles + if abs(abs(horizontal_fov_range[0] - horizontal_fov_range[1]) - 360.0) < 1e-6: + # 360 degree FOV - exclude last point to avoid overlap + expected_num_horizontal = ( + math.ceil((horizontal_fov_range[1] - horizontal_fov_range[0]) / horizontal_res) + 1 + ) - 1 + else: + expected_num_horizontal = ( + math.ceil((horizontal_fov_range[1] - horizontal_fov_range[0]) / horizontal_res) + 1 + ) + + expected_num_rays = channels * expected_num_horizontal + + assert ray_starts.shape[0] == expected_num_rays, ( + f"Expected {expected_num_rays} rays, got {ray_starts.shape[0]} rays. " + f"Horizontal angles: {expected_num_horizontal}, channels: {channels}" + ) + assert ray_directions.shape[0] == expected_num_rays + assert ray_starts.shape[1] == 3 + assert ray_directions.shape[1] == 3 + + def test_lidar_pattern_basic_properties(self, device): + """Test that ray directions are normalized and rays start from origin.""" + cfg = patterns_cfg.LidarPatternCfg( + horizontal_fov_range=(-180.0, 180.0), + horizontal_res=45.0, + channels=8, + vertical_fov_range=(-15.0, 15.0), + ) + ray_starts, ray_directions = patterns.lidar_pattern(cfg, device) + + # Check that all directions are unit vectors + norms = torch.norm(ray_directions, dim=1) + torch.testing.assert_close(norms, torch.ones_like(norms), rtol=1e-5, atol=1e-5) + + # All rays should start from origin + torch.testing.assert_close(ray_starts, torch.zeros_like(ray_starts)) + + @pytest.mark.parametrize( + "vertical_fov_range,channels", + [ + ((-15.0, 15.0), 4), + ((-30.0, 10.0), 5), + ((0.0, 0.0), 1), + ], + ) + def test_lidar_pattern_vertical_channels(self, device, vertical_fov_range, channels): + """Test that vertical channels are distributed correctly.""" + cfg = patterns_cfg.LidarPatternCfg( + horizontal_fov_range=(0.0, 0.0), # Single horizontal direction + horizontal_res=1.0, + channels=channels, + vertical_fov_range=vertical_fov_range, + ) + ray_starts, ray_directions = patterns.lidar_pattern(cfg, device) + + assert ray_starts.shape[0] == channels + + # Check that z-components span the vertical range + # For single horizontal direction (0,0), the z component is sin(vertical_angle) + z_components = ray_directions[:, 2] + expected_min_z = math.sin(math.radians(vertical_fov_range[0])) + expected_max_z = math.sin(math.radians(vertical_fov_range[1])) + + assert torch.isclose(z_components.min(), torch.tensor(expected_min_z, device=device), atol=1e-5) + assert torch.isclose(z_components.max(), torch.tensor(expected_max_z, device=device), atol=1e-5) + + @pytest.mark.parametrize( + "horizontal_fov_range,horizontal_res,expected_num_rays,expected_angular_spacing", + [ + # Test case from the bug fix: 360 deg FOV with 90 deg resolution + ((-180.0, 180.0), 90.0, 4, 90.0), + # Test case: 360 deg FOV with 45 deg resolution + ((-180.0, 180.0), 45.0, 8, 45.0), + # Test case: 180 deg FOV with 90 deg resolution + ((-90.0, 90.0), 90.0, 3, 90.0), + # Test case: 180 deg FOV with 60 deg resolution (avoids atan2 discontinuity at ±180°) + ((-90.0, 90.0), 60.0, 4, 60.0), + # Test case: 360 deg FOV with 120 deg resolution + ((-180.0, 180.0), 120.0, 3, 120.0), + ], + ) + def test_lidar_pattern_exact_angles( + self, device, horizontal_fov_range, horizontal_res, expected_num_rays, expected_angular_spacing + ): + """Test that lidar pattern generates rays with correct count and angular spacing. + + This test verifies the fix for the horizontal angle calculation to ensure + the actual resolution matches the requested resolution. + """ + cfg = patterns_cfg.LidarPatternCfg( + horizontal_fov_range=horizontal_fov_range, + horizontal_res=horizontal_res, + channels=1, + vertical_fov_range=(0.0, 0.0), + ) + ray_starts, ray_directions = patterns.lidar_pattern(cfg, device) + + # Check that we have the right number of rays + assert ray_starts.shape[0] == expected_num_rays, ( + f"Expected {expected_num_rays} rays, got {ray_starts.shape[0]} rays" + ) + + # Calculate angles from directions + angles = torch.atan2(ray_directions[:, 1], ray_directions[:, 0]) + angles_deg = torch.rad2deg(angles) + + # Sort angles for easier checking + angles_deg_sorted = torch.sort(angles_deg)[0] + + # Check angular spacing between consecutive rays + for i in range(len(angles_deg_sorted) - 1): + angular_diff = abs(angles_deg_sorted[i + 1].item() - angles_deg_sorted[i].item()) + # Allow small tolerance for floating point errors + assert abs(angular_diff - expected_angular_spacing) < 1.0, ( + f"Angular spacing {angular_diff:.2f}° does not match expected {expected_angular_spacing}°" + ) + + # For 360 degree FOV, also check that first and last angles wrap correctly + is_360 = abs(abs(horizontal_fov_range[0] - horizontal_fov_range[1]) - 360.0) < 1e-6 + if is_360: + # The gap from last angle back to first angle (wrapping around) should also match spacing + first_angle = angles_deg_sorted[0].item() + last_angle = angles_deg_sorted[-1].item() + wraparound_diff = (first_angle + 360) - last_angle + assert abs(wraparound_diff - expected_angular_spacing) < 1.0, ( + f"Wraparound spacing {wraparound_diff:.2f}° does not match expected {expected_angular_spacing}°" + ) + + +class TestBpearlPattern: + """Test cases for bpearl_pattern function.""" + + @pytest.mark.parametrize( + "horizontal_fov,horizontal_res", + [ + (360.0, 10.0), # Default config + (360.0, 5.0), + (180.0, 10.0), + (90.0, 5.0), + ], + ) + def test_bpearl_pattern_horizontal_params(self, device, horizontal_fov, horizontal_res): + """Test bpearl pattern with different horizontal parameters.""" + cfg = patterns_cfg.BpearlPatternCfg( + horizontal_fov=horizontal_fov, + horizontal_res=horizontal_res, + ) + ray_starts, ray_directions = patterns.bpearl_pattern(cfg, device) + + # Calculate expected number of horizontal angles + expected_num_horizontal = int(horizontal_fov / horizontal_res) + expected_num_rays = len(cfg.vertical_ray_angles) * expected_num_horizontal + + assert ray_starts.shape[0] == expected_num_rays + + def test_bpearl_pattern_basic_properties(self, device): + """Test that ray directions are normalized and rays start from origin.""" + cfg = patterns_cfg.BpearlPatternCfg() + ray_starts, ray_directions = patterns.bpearl_pattern(cfg, device) + + # Check that all directions are unit vectors + norms = torch.norm(ray_directions, dim=1) + torch.testing.assert_close(norms, torch.ones_like(norms), rtol=1e-5, atol=1e-5) + + # All rays should start from origin + torch.testing.assert_close(ray_starts, torch.zeros_like(ray_starts)) + + def test_bpearl_pattern_custom_vertical_angles(self, device): + """Test bpearl pattern with custom vertical angles.""" + custom_angles = [10.0, 20.0, 30.0, 40.0, 50.0] + cfg = patterns_cfg.BpearlPatternCfg( + horizontal_fov=360.0, + horizontal_res=90.0, + vertical_ray_angles=custom_angles, + ) + ray_starts, ray_directions = patterns.bpearl_pattern(cfg, device) + + # 360/90 = 4 horizontal angles, 5 custom vertical angles + expected_num_rays = 4 * 5 + assert ray_starts.shape[0] == expected_num_rays + + +class TestPinholeCameraPattern: + """Test cases for pinhole_camera_pattern function.""" + + @pytest.mark.parametrize( + "width,height", + [ + (640, 480), + (1920, 1080), + (320, 240), + (100, 100), + ], + ) + def test_pinhole_camera_pattern_num_rays(self, device, width, height): + """Test that pinhole camera pattern generates the correct number of rays.""" + cfg = patterns_cfg.PinholeCameraPatternCfg( + width=width, + height=height, + ) + + # Create a simple intrinsic matrix for testing + # Using identity-like matrix with focal lengths and principal point at center + fx = fy = 500.0 + cx = width / 2 + cy = height / 2 + intrinsic_matrix = torch.tensor( + [[fx, 0, cx], [0, fy, cy], [0, 0, 1]], + device=device, + ) + + # Pattern expects batch of intrinsic matrices + intrinsic_matrices = intrinsic_matrix.unsqueeze(0) + + ray_starts, ray_directions = patterns.pinhole_camera_pattern(cfg, intrinsic_matrices, device) + + expected_num_rays = width * height + assert ray_starts.shape == (1, expected_num_rays, 3) + assert ray_directions.shape == (1, expected_num_rays, 3) + + def test_pinhole_camera_pattern_basic_properties(self, device): + """Test that ray directions are normalized and rays start from origin.""" + cfg = patterns_cfg.PinholeCameraPatternCfg(width=100, height=100) + + fx = fy = 500.0 + cx = cy = 50.0 + intrinsic_matrix = torch.tensor( + [[fx, 0, cx], [0, fy, cy], [0, 0, 1]], + device=device, + ).unsqueeze(0) + + ray_starts, ray_directions = patterns.pinhole_camera_pattern(cfg, intrinsic_matrix, device) + + # Check that all directions are unit vectors + norms = torch.norm(ray_directions, dim=2) + torch.testing.assert_close(norms, torch.ones_like(norms), rtol=1e-5, atol=1e-5) + + # All rays should start from origin + torch.testing.assert_close(ray_starts, torch.zeros_like(ray_starts)) + + def test_pinhole_camera_pattern_batch(self, device): + """Test that pinhole camera pattern works with batched intrinsic matrices.""" + cfg = patterns_cfg.PinholeCameraPatternCfg(width=50, height=50) + + # Create batch of 3 different intrinsic matrices + batch_size = 3 + intrinsic_matrices = [] + for i in range(batch_size): + fx = fy = 500.0 + i * 100 + cx = cy = 25.0 + intrinsic_matrices.append(torch.tensor([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], device=device)) + intrinsic_matrices = torch.stack(intrinsic_matrices) + + ray_starts, ray_directions = patterns.pinhole_camera_pattern(cfg, intrinsic_matrices, device) + + expected_num_rays = 50 * 50 + assert ray_starts.shape == (batch_size, expected_num_rays, 3) + assert ray_directions.shape == (batch_size, expected_num_rays, 3) + + # Check that different batches have different ray directions (due to different intrinsics) + assert not torch.allclose(ray_directions[0], ray_directions[1]) + + def test_pinhole_camera_from_intrinsic_matrix(self, device): + """Test creating PinholeCameraPatternCfg from intrinsic matrix.""" + width, height = 640, 480 + fx, fy = 500.0, 500.0 + cx, cy = 320.0, 240.0 + + intrinsic_list = [fx, 0, cx, 0, fy, cy, 0, 0, 1] + + cfg = patterns_cfg.PinholeCameraPatternCfg.from_intrinsic_matrix( + intrinsic_matrix=intrinsic_list, + width=width, + height=height, + ) + + assert cfg.width == width + assert cfg.height == height + assert cfg.focal_length == 24.0 # default + + # The apertures should be calculated based on the intrinsic matrix + assert cfg.horizontal_aperture > 0 + assert cfg.vertical_aperture > 0 diff --git a/source/isaaclab/test/sensors/test_sensor_base.py b/source/isaaclab/test/sensors/test_sensor_base.py index 9f82198214c8..1f41ba4ab4e5 100644 --- a/source/isaaclab/test/sensors/test_sensor_base.py +++ b/source/isaaclab/test/sensors/test_sensor_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,13 +14,11 @@ """Rest everything follows.""" -import torch from collections.abc import Sequence from dataclasses import dataclass -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import pytest +import torch import isaaclab.sim as sim_utils from isaaclab.sensors import SensorBase, SensorBaseCfg @@ -33,7 +31,6 @@ class DummyData: class DummySensor(SensorBase): - def __init__(self, cfg): super().__init__(cfg) self._data = DummyData() @@ -80,7 +77,7 @@ def _populate_scene(): # create prims for i in range(5): - _ = prim_utils.create_prim( + _ = sim_utils.create_prim( f"/World/envs/env_{i:02d}/Cube", "Cube", translation=(i * 1.0, 0.0, 0.0), @@ -90,9 +87,8 @@ def _populate_scene(): @pytest.fixture def create_dummy_sensor(request, device): - # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.01 @@ -105,7 +101,7 @@ def create_dummy_sensor(request, device): sensor_cfg = DummySensorCfg() - stage_utils.update_stage() + sim_utils.update_stage() yield sensor_cfg, sim, dt diff --git a/source/isaaclab/test/sensors/test_tiled_camera.py b/source/isaaclab/test/sensors/test_tiled_camera.py index fdef7a3ae5cf..f160ef35df84 100644 --- a/source/isaaclab/test/sensors/test_tiled_camera.py +++ b/source/isaaclab/test/sensors/test_tiled_camera.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,23 +16,16 @@ """Rest everything follows.""" import copy -import numpy as np import random + +import numpy as np +import pytest import torch -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import omni.replicator.core as rep -import pytest from isaacsim.core.prims import SingleGeometryPrim, SingleRigidPrim from pxr import Gf, UsdGeom -# from Isaac Sim 4.2 onwards, pxr.Semantics is deprecated -try: - import Semantics -except ModuleNotFoundError: - from pxr import Semantics - import isaaclab.sim as sim_utils from isaaclab.sensors.camera import Camera, CameraCfg, TiledCamera, TiledCameraCfg from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -54,7 +47,7 @@ def setup_camera(device) -> tuple[sim_utils.SimulationContext, TiledCameraCfg, f ), ) # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.01 # Load kit helper @@ -63,7 +56,7 @@ def setup_camera(device) -> tuple[sim_utils.SimulationContext, TiledCameraCfg, f # populate scene _populate_scene() # load stage - stage_utils.update_stage() + sim_utils.update_stage() yield sim, camera_cfg, dt # Teardown rep.vp_manager.destroy_hydra_textures("Replicator") @@ -252,7 +245,7 @@ def test_multi_camera_init(setup_camera, device): num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -308,7 +301,7 @@ def test_rgb_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -410,7 +403,7 @@ def test_depth_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -464,7 +457,7 @@ def test_rgba_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -518,7 +511,7 @@ def test_distance_to_camera_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -572,7 +565,7 @@ def test_distance_to_image_plane_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -626,7 +619,7 @@ def test_normals_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -669,6 +662,9 @@ def test_normals_only_camera(setup_camera, device): assert im_data.shape == (num_cameras, camera_cfg.height, camera_cfg.width, 3) for i in range(4): assert im_data[i].mean() > 0.0 + # check normal norm is approximately 1 + norms = torch.norm(im_data, dim=-1) + assert torch.allclose(norms, torch.ones_like(norms), atol=1e-9) assert camera.data.output["normals"].dtype == torch.float del camera @@ -680,7 +676,7 @@ def test_motion_vectors_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -734,7 +730,7 @@ def test_semantic_segmentation_colorize_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -789,7 +785,7 @@ def test_instance_segmentation_fast_colorize_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -844,7 +840,7 @@ def test_instance_id_segmentation_fast_colorize_only_camera(setup_camera, device sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -899,7 +895,7 @@ def test_semantic_segmentation_non_colorize_only_camera(setup_camera, device): sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -956,7 +952,7 @@ def test_instance_segmentation_fast_non_colorize_only_camera(setup_camera, devic sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -1011,7 +1007,7 @@ def test_instance_id_segmentation_fast_non_colorize_only_camera(setup_camera, de sim, camera_cfg, dt = setup_camera num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -1080,7 +1076,7 @@ def test_all_annotators_camera(setup_camera, device): num_cameras = 9 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -1180,7 +1176,7 @@ def test_all_annotators_low_resolution_camera(setup_camera, device): num_cameras = 2 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -1282,7 +1278,7 @@ def test_all_annotators_non_perfect_square_number_camera(setup_camera, device): num_cameras = 11 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform") + sim_utils.create_prim(f"/World/Origin_{i}", "Xform") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -1382,15 +1378,15 @@ def test_all_annotators_instanceable(setup_camera, device): num_cameras = 10 for i in range(num_cameras): - prim_utils.create_prim(f"/World/Origin_{i}", "Xform", translation=(0.0, i, 0.0)) + sim_utils.create_prim(f"/World/Origin_{i}", "Xform", translation=(0.0, i, 0.0)) # Create a stage with 10 instanceable cubes, where each camera points to one cube - stage = stage_utils.get_current_stage() + stage = sim_utils.get_current_stage() for i in range(10): # Remove objects added to stage by default stage.RemovePrim(f"/World/Objects/Obj_{i:02d}") # Add instanceable cubes - prim_utils.create_prim( + sim_utils.create_prim( f"/World/Cube_{i}", "Xform", usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", @@ -1399,11 +1395,7 @@ def test_all_annotators_instanceable(setup_camera, device): scale=(5.0, 5.0, 5.0), ) prim = stage.GetPrimAtPath(f"/World/Cube_{i}") - sem = Semantics.SemanticsAPI.Apply(prim, "Semantics") - sem.CreateSemanticTypeAttr() - sem.CreateSemanticDataAttr() - sem.GetSemanticTypeAttr().Set("class") - sem.GetSemanticDataAttr().Set("cube") + sim_utils.add_labels(prim, labels=["cube"], instance_name="class") # Create camera camera_cfg = copy.deepcopy(camera_cfg) @@ -1651,7 +1643,7 @@ def test_frame_offset_small_resolution(setup_camera, device): # play sim sim.reset() # simulate some steps first to make sure objects are settled - stage = stage_utils.get_current_stage() + stage = sim_utils.get_current_stage() for i in range(10): prim = stage.GetPrimAtPath(f"/World/Objects/Obj_{i:02d}") UsdGeom.Gprim(prim).GetOrderedXformOps()[2].Set(Gf.Vec3d(1.0, 1.0, 1.0)) @@ -1693,7 +1685,7 @@ def test_frame_offset_large_resolution(setup_camera, device): tiled_camera = TiledCamera(camera_cfg) # modify scene to be less stochastic - stage = stage_utils.get_current_stage() + stage = sim_utils.get_current_stage() for i in range(10): prim = stage.GetPrimAtPath(f"/World/Objects/Obj_{i:02d}") color = Gf.Vec3f(1, 1, 1) @@ -1753,7 +1745,7 @@ def _populate_scene(): position *= np.asarray([1.5, 1.5, 0.5]) # create prim prim_type = random.choice(["Cube", "Sphere", "Cylinder"]) - prim = prim_utils.create_prim( + prim = sim_utils.create_prim( f"/World/Objects/Obj_{i:02d}", prim_type, translation=position, diff --git a/source/isaaclab/test/sensors/test_tiled_camera_env.py b/source/isaaclab/test/sensors/test_tiled_camera_env.py index ed7e6e59926f..5c4d33f6a58e 100644 --- a/source/isaaclab/test/sensors/test_tiled_camera_env.py +++ b/source/isaaclab/test/sensors/test_tiled_camera_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -29,12 +29,13 @@ """Rest everything follows.""" -import gymnasium as gym import sys -import omni.usd +import gymnasium as gym import pytest +import omni.usd + from isaaclab.envs import DirectRLEnv, DirectRLEnvCfg, ManagerBasedRLEnv, ManagerBasedRLEnvCfg from isaaclab.sensors import save_images_to_file diff --git a/source/isaaclab/test/sim/check_meshes.py b/source/isaaclab/test/sim/check_meshes.py index 8595605b7478..705677281d3c 100644 --- a/source/isaaclab/test/sim/check_meshes.py +++ b/source/isaaclab/test/sim/check_meshes.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -35,13 +35,12 @@ """Rest everything follows.""" -import numpy as np import random + +import numpy as np import torch import tqdm -import isaacsim.core.utils.prims as prim_utils - import isaaclab.sim as sim_utils @@ -76,7 +75,7 @@ def design_scene(): # create new xform prims for all objects to be spawned under origins = define_origins(num_origins=4, spacing=5.5) for idx, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Origin{idx:02d}", "Xform", translation=origin) + sim_utils.create_prim(f"/World/Origin{idx:02d}", "Xform", translation=origin) # spawn a red cone cfg_sphere = sim_utils.MeshSphereCfg( diff --git a/source/isaaclab/test/sim/test_build_simulation_context_headless.py b/source/isaaclab/test/sim/test_build_simulation_context_headless.py index af29346f9ee5..ebe059bed666 100644 --- a/source/isaaclab/test/sim/test_build_simulation_context_headless.py +++ b/source/isaaclab/test/sim/test_build_simulation_context_headless.py @@ -1,14 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 test has a lot of duplication with ``test_build_simulation_context_nonheadless.py``. This is intentional to ensure that the -tests are run in both headless and non-headless modes, and we currently can't re-build the simulation app in a script. +This test has a lot of duplication with ``test_build_simulation_context_nonheadless.py``. -If you need to make a change to this test, please make sure to also make the same change to ``test_build_simulation_context_nonheadless.py``. +This is intentional to ensure that the tests are run in both headless and non-headless modes, +and we currently can't re-build the simulation app in a script. +If you need to make a change to this test, please make sure to also make the same change to +``test_build_simulation_context_nonheadless.py``. """ """Launch Isaac Sim Simulator first.""" @@ -21,7 +23,6 @@ """Rest everything follows.""" import pytest -from isaacsim.core.utils.prims import is_prim_path_valid from isaaclab.sim.simulation_cfg import SimulationCfg from isaaclab.sim.simulation_context import build_simulation_context @@ -43,16 +44,19 @@ def test_build_simulation_context_no_cfg(gravity_enabled, device, dt): assert sim.cfg.dt == dt # Ensure that dome light didn't get added automatically as we are headless - assert not is_prim_path_valid("/World/defaultDomeLight") + assert not sim.stage.GetPrimAtPath("/World/defaultDomeLight").IsValid() @pytest.mark.parametrize("add_ground_plane", [True, False]) @pytest.mark.isaacsim_ci def test_build_simulation_context_ground_plane(add_ground_plane): """Test that the simulation context is built with the correct ground plane.""" - with build_simulation_context(add_ground_plane=add_ground_plane) as _: + with build_simulation_context(add_ground_plane=add_ground_plane) as sim: # Ensure that ground plane got added - assert is_prim_path_valid("/World/defaultGroundPlane") == add_ground_plane + if add_ground_plane: + assert sim.stage.GetPrimAtPath("/World/defaultGroundPlane").IsValid() + else: + assert not sim.stage.GetPrimAtPath("/World/defaultGroundPlane").IsValid() @pytest.mark.parametrize("add_lighting", [True, False]) @@ -60,13 +64,13 @@ def test_build_simulation_context_ground_plane(add_ground_plane): @pytest.mark.isaacsim_ci def test_build_simulation_context_auto_add_lighting(add_lighting, auto_add_lighting): """Test that the simulation context is built with the correct lighting.""" - with build_simulation_context(add_lighting=add_lighting, auto_add_lighting=auto_add_lighting) as _: + with build_simulation_context(add_lighting=add_lighting, auto_add_lighting=auto_add_lighting) as sim: if add_lighting: # Ensure that dome light got added - assert is_prim_path_valid("/World/defaultDomeLight") + assert sim.stage.GetPrimAtPath("/World/defaultDomeLight").IsValid() else: # Ensure that dome light didn't get added as there's no GUI - assert not is_prim_path_valid("/World/defaultDomeLight") + assert not sim.stage.GetPrimAtPath("/World/defaultDomeLight").IsValid() @pytest.mark.isaacsim_ci diff --git a/source/isaaclab/test/sim/test_build_simulation_context_nonheadless.py b/source/isaaclab/test/sim/test_build_simulation_context_nonheadless.py index 1c1bf480da4e..ae2203c43b70 100644 --- a/source/isaaclab/test/sim/test_build_simulation_context_nonheadless.py +++ b/source/isaaclab/test/sim/test_build_simulation_context_nonheadless.py @@ -1,13 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 test has a lot of duplication with ``test_build_simulation_context_headless.py``. This is intentional to ensure that the -tests are run in both headless and non-headless modes, and we currently can't re-build the simulation app in a script. +"""This test has a lot of duplication with ``test_build_simulation_context_headless.py``. -If you need to make a change to this test, please make sure to also make the same change to ``test_build_simulation_context_headless.py``. +This is intentional to ensure that the tests are run in both headless and non-headless modes, +and we currently can't re-build the simulation app in a script. +If you need to make a change to this test, please make sure to also make the same change to +``test_build_simulation_context_headless.py``. """ """Launch Isaac Sim Simulator first.""" @@ -20,7 +22,6 @@ """Rest everything follows.""" import pytest -from isaacsim.core.utils.prims import is_prim_path_valid from isaaclab.sim.simulation_cfg import SimulationCfg from isaaclab.sim.simulation_context import build_simulation_context @@ -44,22 +45,25 @@ def test_build_simulation_context_no_cfg(gravity_enabled, device, dt): @pytest.mark.parametrize("add_ground_plane", [True, False]) def test_build_simulation_context_ground_plane(add_ground_plane): """Test that the simulation context is built with the correct ground plane.""" - with build_simulation_context(add_ground_plane=add_ground_plane) as _: + with build_simulation_context(add_ground_plane=add_ground_plane) as sim: # Ensure that ground plane got added - assert is_prim_path_valid("/World/defaultGroundPlane") == add_ground_plane + if add_ground_plane: + assert sim.stage.GetPrimAtPath("/World/defaultGroundPlane").IsValid() + else: + assert not sim.stage.GetPrimAtPath("/World/defaultGroundPlane").IsValid() @pytest.mark.parametrize("add_lighting", [True, False]) @pytest.mark.parametrize("auto_add_lighting", [True, False]) def test_build_simulation_context_auto_add_lighting(add_lighting, auto_add_lighting): """Test that the simulation context is built with the correct lighting.""" - with build_simulation_context(add_lighting=add_lighting, auto_add_lighting=auto_add_lighting) as _: + with build_simulation_context(add_lighting=add_lighting, auto_add_lighting=auto_add_lighting) as sim: if auto_add_lighting or add_lighting: # Ensure that dome light got added - assert is_prim_path_valid("/World/defaultDomeLight") + assert sim.stage.GetPrimAtPath("/World/defaultDomeLight").IsValid() else: # Ensure that dome light didn't get added - assert not is_prim_path_valid("/World/defaultDomeLight") + assert not sim.stage.GetPrimAtPath("/World/defaultDomeLight").IsValid() def test_build_simulation_context_cfg(): diff --git a/source/isaaclab/test/sim/test_mesh_converter.py b/source/isaaclab/test/sim/test_mesh_converter.py index 90bfc557c781..ea4529d293cd 100644 --- a/source/isaaclab/test/sim/test_mesh_converter.py +++ b/source/isaaclab/test/sim/test_mesh_converter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,15 +17,15 @@ import random import tempfile -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils -import omni import pytest -from isaacsim.core.api.simulation_context import SimulationContext + +import omni from pxr import UsdGeom, UsdPhysics +import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.sim.converters import MeshConverter, MeshConverterCfg -from isaaclab.sim.schemas import schemas_cfg +from isaaclab.sim.schemas import MESH_APPROXIMATION_TOKENS, schemas_cfg from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path @@ -62,11 +62,11 @@ def assets(): def sim(): """Create a blank new stage for each test.""" # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.01 # Load kit helper - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") + sim = SimulationContext(SimulationCfg(dt=dt)) yield sim # stop simulation sim.stop() @@ -78,16 +78,19 @@ def sim(): def check_mesh_conversion(mesh_converter: MeshConverter): """Check that mesh is loadable and stage is valid.""" + # Obtain stage handle + stage = sim_utils.get_current_stage() + # Load the mesh prim_path = "/World/Object" - prim_utils.create_prim(prim_path, usd_path=mesh_converter.usd_path) + sim_utils.create_prim(prim_path, usd_path=mesh_converter.usd_path) # Check prim can be properly spawned - assert prim_utils.is_prim_path_valid(prim_path) + assert stage.GetPrimAtPath(prim_path).IsValid() # Load a second time prim_path = "/World/Object2" - prim_utils.create_prim(prim_path, usd_path=mesh_converter.usd_path) + sim_utils.create_prim(prim_path, usd_path=mesh_converter.usd_path) # Check prim can be properly spawned - assert prim_utils.is_prim_path_valid(prim_path) + assert stage.GetPrimAtPath(prim_path).IsValid() stage = omni.usd.get_context().get_stage() # Check axis is z-up @@ -97,30 +100,35 @@ def check_mesh_conversion(mesh_converter: MeshConverter): units = UsdGeom.GetStageMetersPerUnit(stage) assert units == 1.0 + # Obtain prim handle + prim = stage.GetPrimAtPath("/World/Object/geometry") # Check mesh settings - pos = tuple(prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:translate").Get()) + pos = tuple(prim.GetAttribute("xformOp:translate").Get()) assert pos == mesh_converter.cfg.translation - quat = prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:orient").Get() + quat = prim.GetAttribute("xformOp:orient").Get() quat = (quat.GetReal(), quat.GetImaginary()[0], quat.GetImaginary()[1], quat.GetImaginary()[2]) assert quat == mesh_converter.cfg.rotation - scale = tuple(prim_utils.get_prim_at_path("/World/Object/geometry").GetAttribute("xformOp:scale").Get()) + scale = tuple(prim.GetAttribute("xformOp:scale").Get()) assert scale == mesh_converter.cfg.scale def check_mesh_collider_settings(mesh_converter: MeshConverter): """Check that mesh collider settings are correct.""" + # Obtain stage handle + stage = sim_utils.get_current_stage() + # Check prim can be properly spawned prim_path = "/World/Object" - prim_utils.create_prim(prim_path, usd_path=mesh_converter.usd_path) - assert prim_utils.is_prim_path_valid(prim_path) + sim_utils.create_prim(prim_path, usd_path=mesh_converter.usd_path) + assert stage.GetPrimAtPath(prim_path).IsValid() # Make uninstanceable to check collision settings - geom_prim = prim_utils.get_prim_at_path(prim_path + "/geometry") + geom_prim = stage.GetPrimAtPath(prim_path + "/geometry") # Check that instancing worked! assert geom_prim.IsInstanceable() == mesh_converter.cfg.make_instanceable # Obtain mesh settings geom_prim.SetInstanceable(False) - mesh_prim = prim_utils.get_prim_at_path(prim_path + "/geometry/mesh") + mesh_prim = stage.GetPrimAtPath(prim_path + "/geometry/mesh") # Check collision settings # -- if collision is enabled, check that API is present @@ -133,16 +141,21 @@ def check_mesh_collider_settings(mesh_converter: MeshConverter): # -- if collision is enabled, check that collision approximation is correct if exp_collision_enabled: if mesh_converter.cfg.mesh_collision_props is not None: - exp_collision_approximation = ( - mesh_converter.cfg.mesh_collision_props.usd_func(mesh_prim).GetApproximationAttr().Get() - ) + exp_collision_approximation_str = mesh_converter.cfg.mesh_collision_props.mesh_approximation_name + exp_collision_approximation_token = MESH_APPROXIMATION_TOKENS[exp_collision_approximation_str] mesh_collision_api = UsdPhysics.MeshCollisionAPI(mesh_prim) collision_approximation = mesh_collision_api.GetApproximationAttr().Get() - assert collision_approximation == exp_collision_approximation, "Collision approximation is not the same!" + # Convert token to string for comparison + assert collision_approximation == exp_collision_approximation_token, ( + "Collision approximation is not the same!" + ) def test_no_change(assets): - """Call conversion twice on the same input asset. This should not generate a new USD file if the hash is the same.""" + """Call conversion twice on the same input asset. + + This should not generate a new USD file if the hash is the same. + """ # create an initial USD file from asset mesh_config = MeshConverterCfg(asset_path=assets["obj"]) mesh_converter = MeshConverter(mesh_config) @@ -255,6 +268,36 @@ def test_collider_convex_hull(assets): check_mesh_collider_settings(mesh_converter) +def test_collider_convex_decomposition(assets): + """Convert an OBJ file using convex decomposition approximation""" + collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) + mesh_collision_prop = schemas_cfg.ConvexDecompositionPropertiesCfg() + mesh_config = MeshConverterCfg( + asset_path=assets["obj"], + mesh_collision_props=mesh_collision_prop, + collision_props=collision_props, + ) + mesh_converter = MeshConverter(mesh_config) + + # check that mesh conversion is successful + check_mesh_collider_settings(mesh_converter) + + +def test_collider_triangle_mesh(assets): + """Convert an OBJ file using triangle mesh approximation""" + collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) + mesh_collision_prop = schemas_cfg.TriangleMeshPropertiesCfg() + mesh_config = MeshConverterCfg( + asset_path=assets["obj"], + mesh_collision_props=mesh_collision_prop, + collision_props=collision_props, + ) + mesh_converter = MeshConverter(mesh_config) + + # check that mesh conversion is successful + check_mesh_collider_settings(mesh_converter) + + def test_collider_mesh_simplification(assets): """Convert an OBJ file using mesh simplification approximation""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) @@ -300,6 +343,21 @@ def test_collider_mesh_bounding_sphere(assets): check_mesh_collider_settings(mesh_converter) +def test_collider_mesh_sdf(assets): + """Convert an OBJ file using signed distance field approximation""" + collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) + mesh_collision_prop = schemas_cfg.SDFMeshPropertiesCfg() + mesh_config = MeshConverterCfg( + asset_path=assets["obj"], + mesh_collision_props=mesh_collision_prop, + collision_props=collision_props, + ) + mesh_converter = MeshConverter(mesh_config) + + # check that mesh conversion is successful + check_mesh_collider_settings(mesh_converter) + + def test_collider_mesh_no_collision(assets): """Convert an OBJ file using bounding sphere with collision disabled""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=False) diff --git a/source/isaaclab/test/sim/test_mjcf_converter.py b/source/isaaclab/test/sim/test_mjcf_converter.py index 5921b12fc6ca..8ce098b4a51b 100644 --- a/source/isaaclab/test/sim/test_mjcf_converter.py +++ b/source/isaaclab/test/sim/test_mjcf_converter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,12 +14,12 @@ import os -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import pytest -from isaacsim.core.api.simulation_context import SimulationContext + from isaacsim.core.utils.extensions import enable_extension, get_extension_path_from_name +import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.sim.converters import MjcfConverter, MjcfConverterCfg @@ -27,11 +27,11 @@ def test_setup_teardown(): """Setup and teardown for each test.""" # Setup: Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Setup: Create simulation context dt = 0.01 - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") + sim = SimulationContext(SimulationCfg(dt=dt)) # Setup: Create MJCF config enable_extension("isaacsim.asset.importer.mjcf") @@ -99,6 +99,6 @@ def test_create_prim_from_usd(test_setup_teardown): urdf_converter = MjcfConverter(mjcf_config) prim_path = "/World/Robot" - prim_utils.create_prim(prim_path, usd_path=urdf_converter.usd_path) + sim_utils.create_prim(prim_path, usd_path=urdf_converter.usd_path) - assert prim_utils.is_prim_path_valid(prim_path) + assert sim.stage.GetPrimAtPath(prim_path).IsValid() diff --git a/source/isaaclab/test/sim/test_schemas.py b/source/isaaclab/test/sim/test_schemas.py index 29b451f42140..05710bd9228e 100644 --- a/source/isaaclab/test/sim/test_schemas.py +++ b/source/isaaclab/test/sim/test_schemas.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,14 +14,13 @@ import math -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import pytest -from isaacsim.core.api.simulation_context import SimulationContext + from pxr import UsdPhysics +import isaaclab.sim as sim_utils import isaaclab.sim.schemas as schemas -from isaaclab.sim.utils import find_global_fixed_joint_prim +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR from isaaclab.utils.string import to_camel_case @@ -30,11 +29,11 @@ def setup_simulation(): """Fixture to set up and tear down the simulation context.""" # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.1 # Load kit helper - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") + sim = SimulationContext(SimulationCfg(dt=dt)) # Set some default values for test arti_cfg = schemas.ArticulationRootPropertiesCfg( enabled_self_collisions=False, @@ -75,6 +74,7 @@ def setup_simulation(): ) yield sim, arti_cfg, rigid_cfg, collision_cfg, mass_cfg, joint_cfg # Teardown + sim._disable_app_control_on_stop_handle = True # prevent timeout sim.stop() sim.clear() sim.clear_all_callbacks() @@ -114,7 +114,7 @@ def test_modify_properties_on_articulation_instanced_usd(setup_simulation): asset_usd_file = f"{ISAAC_NUCLEUS_DIR}/Robots/ANYbotics/anymal_c/anymal_c.usd" if "4.5" in ISAAC_NUCLEUS_DIR: asset_usd_file = asset_usd_file.replace("http", "https").replace("4.5", "5.0") - prim_utils.create_prim("/World/asset_instanced", usd_path=asset_usd_file, translation=(0.0, 0.0, 0.62)) + sim_utils.create_prim("/World/asset_instanced", usd_path=asset_usd_file, translation=(0.0, 0.0, 0.62)) # set properties on the asset and check all properties are set schemas.modify_articulation_root_properties("/World/asset_instanced", arti_cfg) @@ -140,7 +140,7 @@ def test_modify_properties_on_articulation_usd(setup_simulation): asset_usd_file = f"{ISAAC_NUCLEUS_DIR}/Robots/FrankaRobotics/FrankaPanda/franka.usd" if "4.5" in ISAAC_NUCLEUS_DIR: asset_usd_file = asset_usd_file.replace("http", "https").replace("4.5", "5.0") - prim_utils.create_prim("/World/asset", usd_path=asset_usd_file, translation=(0.0, 0.0, 0.62)) + sim_utils.create_prim("/World/asset", usd_path=asset_usd_file, translation=(0.0, 0.0, 0.62)) # set properties on the asset and check all properties are set schemas.modify_articulation_root_properties("/World/asset", arti_cfg) @@ -167,9 +167,9 @@ def test_defining_rigid_body_properties_on_prim(setup_simulation): """Test defining rigid body properties on a prim.""" sim, _, rigid_cfg, collision_cfg, mass_cfg, _ = setup_simulation # create a prim - prim_utils.create_prim("/World/parent", prim_type="XForm") + sim_utils.create_prim("/World/parent", prim_type="XForm") # spawn a prim - prim_utils.create_prim("/World/cube1", prim_type="Cube", translation=(0.0, 0.0, 0.62)) + sim_utils.create_prim("/World/cube1", prim_type="Cube", translation=(0.0, 0.0, 0.62)) # set properties on the asset and check all properties are set schemas.define_rigid_body_properties("/World/cube1", rigid_cfg) schemas.define_collision_properties("/World/cube1", collision_cfg) @@ -180,7 +180,7 @@ def test_defining_rigid_body_properties_on_prim(setup_simulation): _validate_mass_properties_on_prim("/World/cube1", mass_cfg) # spawn another prim - prim_utils.create_prim("/World/cube2", prim_type="Cube", translation=(1.0, 1.0, 0.62)) + sim_utils.create_prim("/World/cube2", prim_type="Cube", translation=(1.0, 1.0, 0.62)) # set properties on the asset and check all properties are set schemas.define_rigid_body_properties("/World/cube2", rigid_cfg) schemas.define_collision_properties("/World/cube2", collision_cfg) @@ -199,13 +199,13 @@ def test_defining_articulation_properties_on_prim(setup_simulation): """Test defining articulation properties on a prim.""" sim, arti_cfg, rigid_cfg, collision_cfg, mass_cfg, _ = setup_simulation # create a parent articulation - prim_utils.create_prim("/World/parent", prim_type="Xform") + sim_utils.create_prim("/World/parent", prim_type="Xform") schemas.define_articulation_root_properties("/World/parent", arti_cfg) # validate the properties _validate_articulation_properties_on_prim("/World/parent", arti_cfg, False) # create a child articulation - prim_utils.create_prim("/World/parent/child", prim_type="Cube", translation=(0.0, 0.0, 0.62)) + sim_utils.create_prim("/World/parent/child", prim_type="Cube", translation=(0.0, 0.0, 0.62)) schemas.define_rigid_body_properties("/World/parent/child", rigid_cfg) schemas.define_mass_properties("/World/parent/child", mass_cfg) @@ -228,8 +228,10 @@ def _validate_articulation_properties_on_prim( If :attr:`has_default_fixed_root` is True, then the asset already has a fixed root link. This is used to check the expected behavior of the fixed root link configuration. """ + # Obtain stage handle + stage = sim_utils.get_current_stage() # the root prim - root_prim = prim_utils.get_prim_at_path(prim_path) + root_prim = stage.GetPrimAtPath(prim_path) # check articulation properties are set correctly for attr_name, attr_value in arti_cfg.__dict__.items(): # skip names we know are not present @@ -238,7 +240,7 @@ def _validate_articulation_properties_on_prim( # handle fixed root link if attr_name == "fix_root_link" and attr_value is not None: # obtain the fixed joint prim - fixed_joint_prim = find_global_fixed_joint_prim(prim_path) + fixed_joint_prim = sim_utils.find_global_fixed_joint_prim(prim_path) # if asset does not have a fixed root link then check if the joint is created if not has_default_fixed_root: if attr_value: @@ -256,9 +258,9 @@ def _validate_articulation_properties_on_prim( # convert attribute name in prim to cfg name prim_prop_name = f"physxArticulation:{to_camel_case(attr_name, to='cC')}" # validate the values - assert root_prim.GetAttribute(prim_prop_name).Get() == pytest.approx( - attr_value, abs=1e-5 - ), f"Failed setting for {prim_prop_name}" + assert root_prim.GetAttribute(prim_prop_name).Get() == pytest.approx(attr_value, abs=1e-5), ( + f"Failed setting for {prim_prop_name}" + ) def _validate_rigid_body_properties_on_prim(prim_path: str, rigid_cfg, verbose: bool = False): @@ -268,8 +270,10 @@ def _validate_rigid_body_properties_on_prim(prim_path: str, rigid_cfg, verbose: Right now this function exploits the hierarchy in the asset to check the properties. This is not a fool-proof way of checking the properties. """ + # Obtain stage handle + stage = sim_utils.get_current_stage() # the root prim - root_prim = prim_utils.get_prim_at_path(prim_path) + root_prim = stage.GetPrimAtPath(prim_path) # check rigid body properties are set correctly for link_prim in root_prim.GetChildren(): if UsdPhysics.RigidBodyAPI(link_prim): @@ -280,9 +284,9 @@ def _validate_rigid_body_properties_on_prim(prim_path: str, rigid_cfg, verbose: # convert attribute name in prim to cfg name prim_prop_name = f"physxRigidBody:{to_camel_case(attr_name, to='cC')}" # validate the values - assert link_prim.GetAttribute(prim_prop_name).Get() == pytest.approx( - attr_value, abs=1e-5 - ), f"Failed setting for {prim_prop_name}" + assert link_prim.GetAttribute(prim_prop_name).Get() == pytest.approx(attr_value, abs=1e-5), ( + f"Failed setting for {prim_prop_name}" + ) elif verbose: print(f"Skipping prim {link_prim.GetPrimPath()} as it is not a rigid body.") @@ -294,8 +298,10 @@ def _validate_collision_properties_on_prim(prim_path: str, collision_cfg, verbos Right now this function exploits the hierarchy in the asset to check the properties. This is not a fool-proof way of checking the properties. """ + # Obtain stage handle + stage = sim_utils.get_current_stage() # the root prim - root_prim = prim_utils.get_prim_at_path(prim_path) + root_prim = stage.GetPrimAtPath(prim_path) # check collision properties are set correctly for link_prim in root_prim.GetChildren(): for mesh_prim in link_prim.GetChildren(): @@ -307,9 +313,9 @@ def _validate_collision_properties_on_prim(prim_path: str, collision_cfg, verbos # convert attribute name in prim to cfg name prim_prop_name = f"physxCollision:{to_camel_case(attr_name, to='cC')}" # validate the values - assert mesh_prim.GetAttribute(prim_prop_name).Get() == pytest.approx( - attr_value, abs=1e-5 - ), f"Failed setting for {prim_prop_name}" + assert mesh_prim.GetAttribute(prim_prop_name).Get() == pytest.approx(attr_value, abs=1e-5), ( + f"Failed setting for {prim_prop_name}" + ) elif verbose: print(f"Skipping prim {mesh_prim.GetPrimPath()} as it is not a collision mesh.") @@ -321,8 +327,10 @@ def _validate_mass_properties_on_prim(prim_path: str, mass_cfg, verbose: bool = Right now this function exploits the hierarchy in the asset to check the properties. This is not a fool-proof way of checking the properties. """ + # Obtain stage handle + stage = sim_utils.get_current_stage() # the root prim - root_prim = prim_utils.get_prim_at_path(prim_path) + root_prim = stage.GetPrimAtPath(prim_path) # check rigid body mass properties are set correctly for link_prim in root_prim.GetChildren(): if UsdPhysics.MassAPI(link_prim): @@ -333,9 +341,9 @@ def _validate_mass_properties_on_prim(prim_path: str, mass_cfg, verbose: bool = # print(link_prim.GetProperties()) prim_prop_name = f"physics:{to_camel_case(attr_name, to='cC')}" # validate the values - assert link_prim.GetAttribute(prim_prop_name).Get() == pytest.approx( - attr_value, abs=1e-5 - ), f"Failed setting for {prim_prop_name}" + assert link_prim.GetAttribute(prim_prop_name).Get() == pytest.approx(attr_value, abs=1e-5), ( + f"Failed setting for {prim_prop_name}" + ) elif verbose: print(f"Skipping prim {link_prim.GetPrimPath()} as it is not a mass api.") @@ -347,8 +355,10 @@ def _validate_joint_drive_properties_on_prim(prim_path: str, joint_cfg, verbose: Right now this function exploits the hierarchy in the asset to check the properties. This is not a fool-proof way of checking the properties. """ + # Obtain stage handle + stage = sim_utils.get_current_stage() # the root prim - root_prim = prim_utils.get_prim_at_path(prim_path) + root_prim = stage.GetPrimAtPath(prim_path) # check joint drive properties are set correctly for link_prim in root_prim.GetAllChildren(): for joint_prim in link_prim.GetChildren(): @@ -392,8 +402,8 @@ def _validate_joint_drive_properties_on_prim(prim_path: str, joint_cfg, verbose: prim_attr_value = prim_attr_value * 180.0 / math.pi # validate the values - assert prim_attr_value == pytest.approx( - attr_value, abs=1e-5 - ), f"Failed setting for {prim_attr_name}" + assert prim_attr_value == pytest.approx(attr_value, abs=1e-5), ( + f"Failed setting for {prim_attr_name}" + ) elif verbose: print(f"Skipping prim {joint_prim.GetPrimPath()} as it is not a joint drive api.") diff --git a/source/isaaclab/test/sim/test_simulation_context.py b/source/isaaclab/test/sim/test_simulation_context.py index f0f783463d2b..4244b36ff8e1 100644 --- a/source/isaaclab/test/sim/test_simulation_context.py +++ b/source/isaaclab/test/sim/test_simulation_context.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,12 +12,14 @@ """Rest everything follows.""" -import numpy as np +from collections.abc import Generator -import isaacsim.core.utils.prims as prim_utils +import numpy as np import pytest -from isaacsim.core.api.simulation_context import SimulationContext as IsaacSimulationContext +import omni.physx + +import isaaclab.sim as sim_utils from isaaclab.sim import SimulationCfg, SimulationContext @@ -34,14 +36,31 @@ def test_setup_teardown(): SimulationContext.clear_instance() +@pytest.fixture +def sim_with_stage_in_memory() -> Generator[SimulationContext, None, None]: + """Create a simulation context with stage in memory.""" + # create stage in memory + cfg = SimulationCfg(create_stage_in_memory=True) + sim = SimulationContext(cfg=cfg) + # update stage + sim_utils.update_stage() + # yield simulation context + yield sim + # stop simulation + omni.physx.get_physx_simulation_interface().detach_stage() + sim.stop() + # clear simulation context + sim.clear() + sim.clear_all_callbacks() + sim.clear_instance() + + @pytest.mark.isaacsim_ci def test_singleton(): """Tests that the singleton is working.""" sim1 = SimulationContext() sim2 = SimulationContext() - sim3 = IsaacSimulationContext() assert sim1 is sim2 - assert sim1 is sim3 # try to delete the singleton sim2.clear_instance() @@ -49,11 +68,7 @@ def test_singleton(): # create new instance sim4 = SimulationContext() assert sim1 is not sim4 - assert sim3 is not sim4 assert sim1.instance() is sim4.instance() - assert sim3.instance() is sim4.instance() - # clear instance - sim3.clear_instance() @pytest.mark.isaacsim_ci @@ -70,8 +85,8 @@ def test_initialization(): assert sim.get_rendering_dt() == cfg.dt * cfg.render_interval assert not sim.has_rtx_sensors() # check valid paths - assert prim_utils.is_prim_path_valid("/Physics/PhysX") - assert prim_utils.is_prim_path_valid("/Physics/PhysX/defaultMaterial") + assert sim.stage.GetPrimAtPath("/Physics/PhysX").IsValid() + assert sim.stage.GetPrimAtPath("/Physics/PhysX/defaultMaterial").IsValid() # check valid gravity gravity_dir, gravity_mag = sim.get_physics_context().get_gravity() gravity = np.array(gravity_dir) * gravity_mag diff --git a/source/isaaclab/test/sim/test_simulation_render_config.py b/source/isaaclab/test/sim/test_simulation_render_config.py index 67b96e9754c1..1bab84d11d7e 100644 --- a/source/isaaclab/test/sim/test_simulation_render_config.py +++ b/source/isaaclab/test/sim/test_simulation_render_config.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,16 +15,16 @@ """Rest everything follows.""" import os -import toml -import carb import flatdict import pytest -from isaacsim.core.utils.carb import get_carb_setting -from isaacsim.core.version import get_version +import toml + +import carb from isaaclab.sim.simulation_cfg import RenderCfg, SimulationCfg from isaaclab.sim.simulation_context import SimulationContext +from isaaclab.utils.version import get_isaac_sim_version @pytest.mark.skip(reason="Timeline not stopped") @@ -108,8 +108,7 @@ def test_render_cfg_presets(): # grab isaac lab apps path isaaclab_app_exp_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), *[".."] * 4, "apps") # for Isaac Sim 4.5 compatibility, we use the 4.5 rendering mode app files in a different folder - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: isaaclab_app_exp_path = os.path.join(isaaclab_app_exp_path, "isaacsim_4_5") # grab preset settings @@ -142,7 +141,7 @@ def test_render_cfg_presets(): # grab groundtruth from preset setting_gt = val - setting_val = get_carb_setting(carb_settings_iface, setting_name) + setting_val = carb_settings_iface.get(setting_name) assert setting_gt == setting_val diff --git a/source/isaaclab/test/sim/test_stage_in_memory.py b/source/isaaclab/test/sim/test_simulation_stage_in_memory.py similarity index 73% rename from source/isaaclab/test/sim/test_stage_in_memory.py rename to source/isaaclab/test/sim/test_simulation_stage_in_memory.py index d114185862aa..68d9d86c666e 100644 --- a/source/isaaclab/test/sim/test_stage_in_memory.py +++ b/source/isaaclab/test/sim/test_simulation_stage_in_memory.py @@ -1,30 +1,33 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 +"""Integration tests for simulation context with stage in memory.""" + """Launch Isaac Sim Simulator first.""" from isaaclab.app import AppLauncher # launch omniverse app +# FIXME (mmittal): Stage in memory requires cameras to be enabled. simulation_app = AppLauncher(headless=True, enable_cameras=True).app """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils + +import pytest + import omni import omni.physx import omni.usd -import pytest import usdrt from isaacsim.core.cloner import GridCloner -from isaacsim.core.version import get_version import isaaclab.sim as sim_utils from isaaclab.sim.simulation_context import SimulationCfg, SimulationContext from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +from isaaclab.utils.version import get_isaac_sim_version @pytest.fixture @@ -32,7 +35,7 @@ def sim(): """Create a simulation context.""" cfg = SimulationCfg(create_stage_in_memory=True) sim = SimulationContext(cfg=cfg) - stage_utils.update_stage() + sim_utils.update_stage() yield sim omni.physx.get_physx_simulation_interface().detach_stage() sim.stop() @@ -50,8 +53,7 @@ def test_stage_in_memory_with_shapes(sim): """Test spawning of shapes with stage in memory.""" # skip test if stage in memory is not supported - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: pytest.skip("Stage in memory is not supported in this version of Isaac Sim") # define parameters @@ -62,19 +64,42 @@ def test_stage_in_memory_with_shapes(sim): with sim_utils.use_stage(stage_in_memory): # create cloned cone stage for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) cfg = sim_utils.MultiAssetSpawnerCfg( assets_cfg=[ sim_utils.ConeCfg( radius=0.3, height=0.6, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + ), ), - sim_utils.CuboidCfg( + sim_utils.MeshCuboidCfg( size=(0.3, 0.3, 0.3), + visual_material=sim_utils.MdlFileCfg( + mdl_path=f"{ISAACLAB_NUCLEUS_DIR}/Materials/TilesMarbleSpiderWhiteBrickBondHoned/TilesMarbleSpiderWhiteBrickBondHoned.mdl", + project_uvw=True, + texture_scale=(0.25, 0.25), + ), ), sim_utils.SphereCfg( radius=0.3, + visual_material=sim_utils.MdlFileCfg( + mdl_path=f"{ISAACLAB_NUCLEUS_DIR}/Materials/TilesMarbleSpiderWhiteBrickBondHoned/TilesMarbleSpiderWhiteBrickBondHoned.mdl", + project_uvw=True, + texture_scale=(0.25, 0.25), + ), + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + ), ), ], random_choice=True, @@ -91,13 +116,13 @@ def test_stage_in_memory_with_shapes(sim): assert sim_utils.is_current_stage_in_memory() # verify prims exist in stage in memory - prims = prim_utils.find_matching_prim_paths(prim_path_regex) + prims = sim_utils.find_matching_prim_paths(prim_path_regex) assert len(prims) == num_clones # verify prims do not exist in context stage context_stage = omni.usd.get_context().get_stage() with sim_utils.use_stage(context_stage): - prims = prim_utils.find_matching_prim_paths(prim_path_regex) + prims = sim_utils.find_matching_prim_paths(prim_path_regex) assert len(prims) != num_clones # attach stage to context @@ -107,7 +132,7 @@ def test_stage_in_memory_with_shapes(sim): assert not sim_utils.is_current_stage_in_memory() # verify prims now exist in context stage - prims = prim_utils.find_matching_prim_paths(prim_path_regex) + prims = sim_utils.find_matching_prim_paths(prim_path_regex) assert len(prims) == num_clones @@ -115,8 +140,7 @@ def test_stage_in_memory_with_usds(sim): """Test spawning of USDs with stage in memory.""" # skip test if stage in memory is not supported - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: pytest.skip("Stage in memory is not supported in this version of Isaac Sim") # define parameters @@ -131,7 +155,7 @@ def test_stage_in_memory_with_usds(sim): with sim_utils.use_stage(stage_in_memory): # create cloned robot stage for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) cfg = sim_utils.MultiUsdFileCfg( usd_path=usd_paths, @@ -157,13 +181,13 @@ def test_stage_in_memory_with_usds(sim): assert sim_utils.is_current_stage_in_memory() # verify prims exist in stage in memory - prims = prim_utils.find_matching_prim_paths(prim_path_regex) + prims = sim_utils.find_matching_prim_paths(prim_path_regex) assert len(prims) == num_clones # verify prims do not exist in context stage context_stage = omni.usd.get_context().get_stage() with sim_utils.use_stage(context_stage): - prims = prim_utils.find_matching_prim_paths(prim_path_regex) + prims = sim_utils.find_matching_prim_paths(prim_path_regex) assert len(prims) != num_clones # attach stage to context @@ -173,7 +197,7 @@ def test_stage_in_memory_with_usds(sim): assert not sim_utils.is_current_stage_in_memory() # verify prims now exist in context stage - prims = prim_utils.find_matching_prim_paths(prim_path_regex) + prims = sim_utils.find_matching_prim_paths(prim_path_regex) assert len(prims) == num_clones @@ -181,8 +205,7 @@ def test_stage_in_memory_with_clone_in_fabric(sim): """Test cloning in fabric with stage in memory.""" # skip test if stage in memory is not supported - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: pytest.skip("Stage in memory is not supported in this version of Isaac Sim") # define parameters @@ -201,7 +224,7 @@ def test_stage_in_memory_with_clone_in_fabric(sim): cloner.define_base_env(base_env_path) # create source prim - prim_utils.create_prim(f"{source_prim_path}/Robot", "Xform", usd_path=usd_path) + sim_utils.create_prim(f"{source_prim_path}/Robot", "Xform", usd_path=usd_path) # generate target paths target_paths = cloner.generate_paths("/World/envs/env", num_clones) @@ -219,7 +242,7 @@ def test_stage_in_memory_with_clone_in_fabric(sim): # verify prims do not exist in context stage context_stage = omni.usd.get_context().get_stage() with sim_utils.use_stage(context_stage): - prims = prim_utils.find_matching_prim_paths(prim_path_regex) + prims = sim_utils.find_matching_prim_paths(prim_path_regex) assert len(prims) != num_clones # attach stage to context diff --git a/source/isaaclab/test/sim/test_spawn_from_files.py b/source/isaaclab/test/sim/test_spawn_from_files.py index 59b2741e4eee..9edf535ac91e 100644 --- a/source/isaaclab/test/sim/test_spawn_from_files.py +++ b/source/isaaclab/test/sim/test_spawn_from_files.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,27 +12,28 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import pytest -from isaacsim.core.api.simulation_context import SimulationContext -from isaacsim.core.utils.extensions import enable_extension, get_extension_path_from_name +from packaging.version import Version + +import omni.kit.app import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +from isaaclab.utils.version import get_isaac_sim_version @pytest.fixture def sim(): """Create a blank new stage for each test.""" # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.1 # Load kit helper - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") + sim = SimulationContext(SimulationCfg(dt=dt)) # Wait for spawning - stage_utils.update_stage() + sim_utils.update_stage() yield sim @@ -51,7 +52,7 @@ def test_spawn_usd(sim): prim = cfg.func("/World/Franka", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Franka") + assert sim.stage.GetPrimAtPath("/World/Franka").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" @@ -68,9 +69,16 @@ def test_spawn_usd_fails(sim): @pytest.mark.isaacsim_ci def test_spawn_urdf(sim): """Test loading prim from URDF file.""" + # pin the urdf importer extension to the older version + manager = omni.kit.app.get_app().get_extension_manager() + if get_isaac_sim_version() >= Version("5.1"): + pinned_urdf_extension_name = "isaacsim.asset.importer.urdf-2.4.31" + manager.set_extension_enabled_immediate(pinned_urdf_extension_name, True) + else: + pinned_urdf_extension_name = "isaacsim.asset.importer.urdf" # retrieve path to urdf importer extension - enable_extension("isaacsim.asset.importer.urdf") - extension_path = get_extension_path_from_name("isaacsim.asset.importer.urdf") + extension_id = manager.get_enabled_extension_id(pinned_urdf_extension_name) + extension_path = manager.get_extension_path(extension_id) # Spawn franka from URDF cfg = sim_utils.UrdfFileCfg( asset_path=f"{extension_path}/data/urdf/robots/franka_description/robots/panda_arm_hand.urdf", @@ -82,7 +90,7 @@ def test_spawn_urdf(sim): prim = cfg.func("/World/Franka", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Franka") + assert sim.stage.GetPrimAtPath("/World/Franka").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" @@ -94,5 +102,104 @@ def test_spawn_ground_plane(sim): prim = cfg.func("/World/ground_plane", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/ground_plane") + assert sim.stage.GetPrimAtPath("/World/ground_plane").IsValid() + assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" + + +@pytest.mark.isaacsim_ci +def test_spawn_usd_with_compliant_contact_material(sim): + """Test loading prim from USD file with physics material applied to specific prim.""" + # Spawn gelsight finger with physics material on specific prim + usd_file_path = f"{ISAACLAB_NUCLEUS_DIR}/TacSL/gelsight_r15_finger/gelsight_r15_finger.usd" + + # Create spawn configuration + spawn_cfg = sim_utils.UsdFileWithCompliantContactCfg( + usd_path=usd_file_path, + rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=True), + compliant_contact_stiffness=1000.0, + compliant_contact_damping=100.0, + physics_material_prim_path="elastomer", + ) + + # Spawn the prim + prim = spawn_cfg.func("/World/Robot", spawn_cfg) + + # Check validity + assert prim.IsValid() + assert sim.stage.GetPrimAtPath("/World/Robot").IsValid() + assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" + + material_prim_path = "/World/Robot/elastomer/compliant_material" + # Check that the physics material was applied to the specified prim + assert sim.stage.GetPrimAtPath(material_prim_path).IsValid() + + # Check properties + material_prim = sim.stage.GetPrimAtPath(material_prim_path) + assert material_prim.IsValid() + assert material_prim.GetAttribute("physxMaterial:compliantContactStiffness").Get() == 1000.0 + assert material_prim.GetAttribute("physxMaterial:compliantContactDamping").Get() == 100.0 + + +@pytest.mark.isaacsim_ci +def test_spawn_usd_with_compliant_contact_material_on_multiple_prims(sim): + """Test loading prim from USD file with physics material applied to multiple prims.""" + # Spawn Panda robot with physics material on specific prims + usd_file_path = f"{ISAACLAB_NUCLEUS_DIR}/TacSL/gelsight_r15_finger/gelsight_r15_finger.usd" + + # Create spawn configuration + spawn_cfg = sim_utils.UsdFileWithCompliantContactCfg( + usd_path=usd_file_path, + rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=True), + compliant_contact_stiffness=1000.0, + compliant_contact_damping=100.0, + physics_material_prim_path=["elastomer", "gelsight_finger"], + ) + + # Spawn the prim + prim = spawn_cfg.func("/World/Robot", spawn_cfg) + + # Check validity + assert prim.IsValid() + assert sim.stage.GetPrimAtPath("/World/Robot").IsValid() + assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" + + # Check that the physics material was applied to the specified prims + for link_name in ["elastomer", "gelsight_finger"]: + material_prim_path = f"/World/Robot/{link_name}/compliant_material" + print("checking", material_prim_path) + assert sim.stage.GetPrimAtPath(material_prim_path).IsValid() + + # Check properties + material_prim = sim.stage.GetPrimAtPath(material_prim_path) + assert material_prim.IsValid() + assert material_prim.GetAttribute("physxMaterial:compliantContactStiffness").Get() == 1000.0 + assert material_prim.GetAttribute("physxMaterial:compliantContactDamping").Get() == 100.0 + + +@pytest.mark.isaacsim_ci +def test_spawn_usd_with_compliant_contact_material_no_prim_path(sim): + """Test loading prim from USD file with physics material but no prim path specified.""" + # Spawn gelsight finger without specifying prim path for physics material + usd_file_path = f"{ISAACLAB_NUCLEUS_DIR}/TacSL/gelsight_r15_finger/gelsight_r15_finger.usd" + + # Create spawn configuration without physics material prim path + spawn_cfg = sim_utils.UsdFileWithCompliantContactCfg( + usd_path=usd_file_path, + rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=True), + compliant_contact_stiffness=1000.0, + compliant_contact_damping=100.0, + physics_material_prim_path=None, + ) + + # Spawn the prim + prim = spawn_cfg.func("/World/Robot", spawn_cfg) + + # Check validity - should still spawn successfully but without physics material + assert prim.IsValid() + assert sim.stage.GetPrimAtPath("/World/Robot").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" + + material_prim_path = "/World/Robot/elastomer/compliant_material" + material_prim = sim.stage.GetPrimAtPath(material_prim_path) + assert material_prim is not None + assert not material_prim.IsValid() diff --git a/source/isaaclab/test/sim/test_spawn_lights.py b/source/isaaclab/test/sim/test_spawn_lights.py index ec178244e1b2..9dbbd98cf7a2 100644 --- a/source/isaaclab/test/sim/test_spawn_lights.py +++ b/source/isaaclab/test/sim/test_spawn_lights.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,27 +12,27 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils + import pytest -from isaacsim.core.api.simulation_context import SimulationContext -from pxr import UsdLux + +from pxr import Usd, UsdLux import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils.string import to_camel_case @pytest.fixture(autouse=True) -def test_setup_teardown(): +def sim(): """Setup and teardown for each test.""" # Setup: Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.1 # Load kit helper - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") + sim = SimulationContext(SimulationCfg(dt=dt)) # Wait for spawning - stage_utils.update_stage() + sim_utils.update_stage() # Yield the simulation context for the test yield sim @@ -44,43 +44,7 @@ def test_setup_teardown(): sim.clear_instance() -def _validate_properties_on_prim(prim_path: str, cfg: sim_utils.LightCfg): - """Validate the properties on the prim. - - Args: - prim_path: The prim name. - cfg: The configuration for the light source. - """ - # default list of params to skip - non_usd_params = ["func", "prim_type", "visible", "semantic_tags", "copy_from_source"] - # obtain prim - prim = prim_utils.get_prim_at_path(prim_path) - for attr_name, attr_value in cfg.__dict__.items(): - # skip names we know are not present - if attr_name in non_usd_params or attr_value is None: - continue - # deal with texture input names - if "texture" in attr_name: - light_prim = UsdLux.DomeLight(prim) - if attr_name == "texture_file": - configured_value = light_prim.GetTextureFileAttr().Get() - elif attr_name == "texture_format": - configured_value = light_prim.GetTextureFormatAttr().Get() - else: - raise ValueError(f"Unknown texture attribute: '{attr_name}'") - else: - # convert attribute name in prim to cfg name - if attr_name == "visible_in_primary_ray": - prim_prop_name = f"{to_camel_case(attr_name, to='cC')}" - else: - prim_prop_name = f"inputs:{to_camel_case(attr_name, to='cC')}" - # configured value - configured_value = prim.GetAttribute(prim_prop_name).Get() - # validate the values - assert configured_value == attr_value, f"Failed for attribute: '{attr_name}'" - - -def test_spawn_disk_light(test_setup_teardown): +def test_spawn_disk_light(sim): """Test spawning a disk light source.""" cfg = sim_utils.DiskLightCfg( color=(0.1, 0.1, 0.1), enable_color_temperature=True, color_temperature=5500, intensity=100, radius=20.0 @@ -89,13 +53,13 @@ def test_spawn_disk_light(test_setup_teardown): # check if the light is spawned assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/disk_light") + assert sim.stage.GetPrimAtPath("/World/disk_light").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "DiskLight" # validate properties on the prim - _validate_properties_on_prim("/World/disk_light", cfg) + _validate_properties_on_prim(prim, cfg) -def test_spawn_distant_light(test_setup_teardown): +def test_spawn_distant_light(sim): """Test spawning a distant light.""" cfg = sim_utils.DistantLightCfg( color=(0.1, 0.1, 0.1), enable_color_temperature=True, color_temperature=5500, intensity=100, angle=20 @@ -104,13 +68,13 @@ def test_spawn_distant_light(test_setup_teardown): # check if the light is spawned assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/distant_light") + assert sim.stage.GetPrimAtPath("/World/distant_light").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "DistantLight" # validate properties on the prim - _validate_properties_on_prim("/World/distant_light", cfg) + _validate_properties_on_prim(prim, cfg) -def test_spawn_dome_light(test_setup_teardown): +def test_spawn_dome_light(sim): """Test spawning a dome light source.""" cfg = sim_utils.DomeLightCfg( color=(0.1, 0.1, 0.1), enable_color_temperature=True, color_temperature=5500, intensity=100 @@ -119,13 +83,13 @@ def test_spawn_dome_light(test_setup_teardown): # check if the light is spawned assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/dome_light") + assert sim.stage.GetPrimAtPath("/World/dome_light").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "DomeLight" # validate properties on the prim - _validate_properties_on_prim("/World/dome_light", cfg) + _validate_properties_on_prim(prim, cfg) -def test_spawn_cylinder_light(test_setup_teardown): +def test_spawn_cylinder_light(sim): """Test spawning a cylinder light source.""" cfg = sim_utils.CylinderLightCfg( color=(0.1, 0.1, 0.1), enable_color_temperature=True, color_temperature=5500, intensity=100, radius=20.0 @@ -134,13 +98,13 @@ def test_spawn_cylinder_light(test_setup_teardown): # check if the light is spawned assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/cylinder_light") + assert sim.stage.GetPrimAtPath("/World/cylinder_light").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "CylinderLight" # validate properties on the prim - _validate_properties_on_prim("/World/cylinder_light", cfg) + _validate_properties_on_prim(prim, cfg) -def test_spawn_sphere_light(test_setup_teardown): +def test_spawn_sphere_light(sim): """Test spawning a sphere light source.""" cfg = sim_utils.SphereLightCfg( color=(0.1, 0.1, 0.1), enable_color_temperature=True, color_temperature=5500, intensity=100, radius=20.0 @@ -149,7 +113,47 @@ def test_spawn_sphere_light(test_setup_teardown): # check if the light is spawned assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/sphere_light") + assert sim.stage.GetPrimAtPath("/World/sphere_light").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "SphereLight" # validate properties on the prim - _validate_properties_on_prim("/World/sphere_light", cfg) + _validate_properties_on_prim(prim, cfg) + + +""" +Helper functions. +""" + + +def _validate_properties_on_prim(prim: Usd.Prim, cfg: sim_utils.LightCfg): + """Validate the properties on the prim. + + Args: + prim: The prim. + cfg: The configuration for the light source. + """ + # default list of params to skip + non_usd_params = ["func", "prim_type", "visible", "semantic_tags", "copy_from_source"] + # validate the properties + for attr_name, attr_value in cfg.__dict__.items(): + # skip names we know are not present + if attr_name in non_usd_params or attr_value is None: + continue + # deal with texture input names + if "texture" in attr_name: + light_prim = UsdLux.DomeLight(prim) + if attr_name == "texture_file": + configured_value = light_prim.GetTextureFileAttr().Get() + elif attr_name == "texture_format": + configured_value = light_prim.GetTextureFormatAttr().Get() + else: + raise ValueError(f"Unknown texture attribute: '{attr_name}'") + else: + # convert attribute name in prim to cfg name + if attr_name == "visible_in_primary_ray": + prim_prop_name = f"{to_camel_case(attr_name, to='cC')}" + else: + prim_prop_name = f"inputs:{to_camel_case(attr_name, to='cC')}" + # configured value + configured_value = prim.GetAttribute(prim_prop_name).Get() + # validate the values + assert configured_value == attr_value, f"Failed for attribute: '{attr_name}'" diff --git a/source/isaaclab/test/sim/test_spawn_materials.py b/source/isaaclab/test/sim/test_spawn_materials.py index e95ee6e3724f..e5c3b14f50d7 100644 --- a/source/isaaclab/test/sim/test_spawn_materials.py +++ b/source/isaaclab/test/sim/test_spawn_materials.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,23 +12,23 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils + import pytest -from isaacsim.core.api.simulation_context import SimulationContext + from pxr import UsdPhysics, UsdShade import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils.assets import NVIDIA_NUCLEUS_DIR @pytest.fixture def sim(): """Create a simulation context.""" - stage_utils.create_new_stage() + sim_utils.create_new_stage() dt = 0.1 - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") - stage_utils.update_stage() + sim = SimulationContext(SimulationCfg(dt=dt)) + sim_utils.update_stage() yield sim sim.stop() sim.clear() @@ -42,7 +42,7 @@ def test_spawn_preview_surface(sim): prim = cfg.func("/Looks/PreviewSurface", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/Looks/PreviewSurface") + assert sim.stage.GetPrimAtPath("/Looks/PreviewSurface").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Shader" # Check properties assert prim.GetAttribute("inputs:diffuseColor").Get() == cfg.diffuse_color @@ -58,7 +58,7 @@ def test_spawn_mdl_material(sim): prim = cfg.func("/Looks/MdlMaterial", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/Looks/MdlMaterial") + assert sim.stage.GetPrimAtPath("/Looks/MdlMaterial").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Shader" # Check properties assert prim.GetAttribute("inputs:project_uvw").Get() == cfg.project_uvw @@ -71,7 +71,7 @@ def test_spawn_glass_mdl_material(sim): prim = cfg.func("/Looks/GlassMaterial", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/Looks/GlassMaterial") + assert sim.stage.GetPrimAtPath("/Looks/GlassMaterial").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Shader" # Check properties assert prim.GetAttribute("inputs:thin_walled").Get() == cfg.thin_walled @@ -91,7 +91,7 @@ def test_spawn_rigid_body_material(sim): prim = cfg.func("/Looks/RigidBodyMaterial", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/Looks/RigidBodyMaterial") + assert sim.stage.GetPrimAtPath("/Looks/RigidBodyMaterial").IsValid() # Check properties assert prim.GetAttribute("physics:staticFriction").Get() == cfg.static_friction assert prim.GetAttribute("physics:dynamicFriction").Get() == cfg.dynamic_friction @@ -113,7 +113,7 @@ def test_spawn_deformable_body_material(sim): prim = cfg.func("/Looks/DeformableBodyMaterial", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/Looks/DeformableBodyMaterial") + assert sim.stage.GetPrimAtPath("/Looks/DeformableBodyMaterial").IsValid() # Check properties assert prim.GetAttribute("physxDeformableBodyMaterial:density").Get() == cfg.density assert prim.GetAttribute("physxDeformableBodyMaterial:dynamicFriction").Get() == cfg.dynamic_friction @@ -139,7 +139,7 @@ def test_apply_rigid_body_material_on_visual_material(sim): prim = cfg.func("/Looks/Material", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/Looks/Material") + assert sim.stage.GetPrimAtPath("/Looks/Material").IsValid() # Check properties assert prim.GetAttribute("physics:staticFriction").Get() == cfg.static_friction assert prim.GetAttribute("physics:dynamicFriction").Get() == cfg.dynamic_friction @@ -152,7 +152,7 @@ def test_bind_prim_to_material(sim): """Test binding a rigid body material on a mesh prim.""" # create a mesh prim - object_prim = prim_utils.create_prim("/World/Geometry/box", "Cube") + object_prim = sim_utils.create_prim("/World/Geometry/box", "Cube") UsdPhysics.CollisionAPI.Apply(object_prim) # create a visual material diff --git a/source/isaaclab/test/sim/test_spawn_meshes.py b/source/isaaclab/test/sim/test_spawn_meshes.py index b2297255d974..43fbc7852c2a 100644 --- a/source/isaaclab/test/sim/test_spawn_meshes.py +++ b/source/isaaclab/test/sim/test_spawn_meshes.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,27 +12,27 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils + import pytest -from isaacsim.core.api.simulation_context import SimulationContext import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext @pytest.fixture def sim(): """Create a simulation context for testing.""" # Create a new stage - stage_utils.create_new_stage() + sim_utils.create_new_stage() # Simulation time-step dt = 0.1 # Load kit helper - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, device="cuda:0") + sim = SimulationContext(SimulationCfg(dt=dt)) # Wait for spawning - stage_utils.update_stage() + sim_utils.update_stage() yield sim # Cleanup + sim._disable_app_control_on_stop_handle = True # prevent timeout sim.stop() sim.clear() sim.clear_all_callbacks() @@ -49,12 +49,13 @@ def test_spawn_cone(sim): # Spawn cone cfg = sim_utils.MeshConeCfg(radius=1.0, height=2.0, axis="Y") prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh" @@ -63,12 +64,13 @@ def test_spawn_capsule(sim): # Spawn capsule cfg = sim_utils.MeshCapsuleCfg(radius=1.0, height=2.0, axis="Y") prim = cfg.func("/World/Capsule", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Capsule") + assert sim.stage.GetPrimAtPath("/World/Capsule").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Capsule/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Capsule/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh" @@ -77,12 +79,13 @@ def test_spawn_cylinder(sim): # Spawn cylinder cfg = sim_utils.MeshCylinderCfg(radius=1.0, height=2.0, axis="Y") prim = cfg.func("/World/Cylinder", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cylinder") + assert sim.stage.GetPrimAtPath("/World/Cylinder").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Cylinder/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cylinder/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh" @@ -91,12 +94,13 @@ def test_spawn_cuboid(sim): # Spawn cuboid cfg = sim_utils.MeshCuboidCfg(size=(1.0, 2.0, 3.0)) prim = cfg.func("/World/Cube", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cube") + assert sim.stage.GetPrimAtPath("/World/Cube").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Cube/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cube/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh" @@ -105,12 +109,13 @@ def test_spawn_sphere(sim): # Spawn sphere cfg = sim_utils.MeshSphereCfg(radius=1.0) prim = cfg.func("/World/Sphere", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Sphere") + assert sim.stage.GetPrimAtPath("/World/Sphere").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Sphere/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Sphere/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Mesh" @@ -128,13 +133,14 @@ def test_spawn_cone_with_deformable_props(sim): deformable_props=sim_utils.DeformableBodyPropertiesCfg(deformable_enabled=True), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() # Check properties # Unlike rigid body, deformable body properties are on the mesh prim - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetAttribute("physxDeformable:deformableEnabled").Get() == cfg.deformable_props.deformable_enabled @@ -148,11 +154,12 @@ def test_spawn_cone_with_deformable_and_mass_props(sim): mass_props=sim_utils.MassPropertiesCfg(mass=1.0), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() # Check properties - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetAttribute("physics:mass").Get() == cfg.mass_props.mass # check sim playing @@ -177,11 +184,12 @@ def test_spawn_cone_with_deformable_and_density_props(sim): mass_props=sim_utils.MassPropertiesCfg(density=10.0), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() # Check properties - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetAttribute("physics:density").Get() == cfg.mass_props.density # check sim playing sim.play() @@ -201,13 +209,14 @@ def test_spawn_cone_with_all_deformable_props(sim): physics_material=sim_utils.materials.DeformableBodyMaterialCfg(), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") - assert prim_utils.is_prim_path_valid("/World/Cone/geometry/material") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() + assert sim.stage.GetPrimAtPath("/World/Cone/geometry/material").IsValid() # Check properties # -- deformable body - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetAttribute("physxDeformable:deformableEnabled").Get() is True # check sim playing @@ -231,13 +240,14 @@ def test_spawn_cone_with_all_rigid_props(sim): physics_material=sim_utils.materials.RigidBodyMaterialCfg(), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") - assert prim_utils.is_prim_path_valid("/World/Cone/geometry/material") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() + assert sim.stage.GetPrimAtPath("/World/Cone/geometry/material").IsValid() # Check properties # -- rigid body - prim = prim_utils.get_prim_at_path("/World/Cone") + prim = sim.stage.GetPrimAtPath("/World/Cone") assert prim.GetAttribute("physics:rigidBodyEnabled").Get() == cfg.rigid_props.rigid_body_enabled assert ( prim.GetAttribute("physxRigidBody:solverPositionIterationCount").Get() @@ -247,7 +257,7 @@ def test_spawn_cone_with_all_rigid_props(sim): # -- mass assert prim.GetAttribute("physics:mass").Get() == cfg.mass_props.mass # -- collision shape - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetAttribute("physics:collisionEnabled").Get() is True # check sim playing diff --git a/source/isaaclab/test/sim/test_spawn_sensors.py b/source/isaaclab/test/sim/test_spawn_sensors.py index ac0cab828ad1..63f29af7830c 100644 --- a/source/isaaclab/test/sim/test_spawn_sensors.py +++ b/source/isaaclab/test/sim/test_spawn_sensors.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,12 +12,13 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils + import pytest -from isaacsim.core.api.simulation_context import SimulationContext + +from pxr import Usd import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.sim.spawners.sensors.sensors import CUSTOM_FISHEYE_CAMERA_ATTRIBUTES, CUSTOM_PINHOLE_CAMERA_ATTRIBUTES from isaaclab.utils.string import to_camel_case @@ -25,10 +26,10 @@ @pytest.fixture def sim(): """Create a simulation context.""" - stage_utils.create_new_stage() + sim_utils.create_new_stage() dt = 0.1 - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") - stage_utils.update_stage() + sim = SimulationContext(SimulationCfg(dt=dt)) + sim_utils.update_stage() yield sim sim.stop() sim.clear() @@ -49,10 +50,10 @@ def test_spawn_pinhole_camera(sim): prim = cfg.func("/World/pinhole_camera", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/pinhole_camera") + assert sim.stage.GetPrimAtPath("/World/pinhole_camera").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Camera" # Check properties - _validate_properties_on_prim("/World/pinhole_camera", cfg, CUSTOM_PINHOLE_CAMERA_ATTRIBUTES) + _validate_properties_on_prim(prim, cfg, CUSTOM_PINHOLE_CAMERA_ATTRIBUTES) def test_spawn_fisheye_camera(sim): @@ -69,10 +70,10 @@ def test_spawn_fisheye_camera(sim): prim = cfg.func("/World/fisheye_camera", cfg) # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/fisheye_camera") + assert sim.stage.GetPrimAtPath("/World/fisheye_camera").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Camera" # Check properties - _validate_properties_on_prim("/World/fisheye_camera", cfg, CUSTOM_FISHEYE_CAMERA_ATTRIBUTES) + _validate_properties_on_prim(prim, cfg, CUSTOM_FISHEYE_CAMERA_ATTRIBUTES) """ @@ -80,15 +81,14 @@ def test_spawn_fisheye_camera(sim): """ -def _validate_properties_on_prim(prim_path: str, cfg: object, custom_attr: dict): +def _validate_properties_on_prim(prim: Usd.Prim, cfg: object, custom_attr: dict): """Validate the properties on the prim. Args: - prim_path: The prim name. + prim: The prim. cfg: The configuration object. custom_attr: The custom attributes for sensor. """ - # delete custom attributes in the config that are not USD parameters non_usd_cfg_param_names = [ "func", @@ -98,8 +98,7 @@ def _validate_properties_on_prim(prim_path: str, cfg: object, custom_attr: dict) "semantic_tags", "from_intrinsic_matrix", ] - # get prim - prim = prim_utils.get_prim_at_path(prim_path) + # validate the properties for attr_name, attr_value in cfg.__dict__.items(): # skip names we know are not present if attr_name in non_usd_cfg_param_names or attr_value is None: diff --git a/source/isaaclab/test/sim/test_spawn_shapes.py b/source/isaaclab/test/sim/test_spawn_shapes.py index c889a4ab8181..4c18753d52ea 100644 --- a/source/isaaclab/test/sim/test_spawn_shapes.py +++ b/source/isaaclab/test/sim/test_spawn_shapes.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,22 +12,21 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils import pytest -from isaacsim.core.api.simulation_context import SimulationContext import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext @pytest.fixture def sim(): """Create a simulation context.""" - stage_utils.create_new_stage() + sim_utils.create_new_stage() dt = 0.1 - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") - stage_utils.update_stage() + sim = SimulationContext(SimulationCfg(dt=dt)) + sim_utils.update_stage() yield sim + sim._disable_app_control_on_stop_handle = True # prevent timeout sim.stop() sim.clear() sim.clear_all_callbacks() @@ -43,12 +42,12 @@ def test_spawn_cone(sim): """Test spawning of UsdGeom.Cone prim.""" cfg = sim_utils.ConeCfg(radius=1.0, height=2.0, axis="Y") prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Cone" assert prim.GetAttribute("radius").Get() == cfg.radius assert prim.GetAttribute("height").Get() == cfg.height @@ -59,10 +58,13 @@ def test_spawn_capsule(sim): """Test spawning of UsdGeom.Capsule prim.""" cfg = sim_utils.CapsuleCfg(radius=1.0, height=2.0, axis="Y") prim = cfg.func("/World/Capsule", cfg) + + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Capsule") + assert sim.stage.GetPrimAtPath("/World/Capsule").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" - prim = prim_utils.get_prim_at_path("/World/Capsule/geometry/mesh") + # Check properties + prim = sim.stage.GetPrimAtPath("/World/Capsule/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Capsule" assert prim.GetAttribute("radius").Get() == cfg.radius assert prim.GetAttribute("height").Get() == cfg.height @@ -73,12 +75,13 @@ def test_spawn_cylinder(sim): """Test spawning of UsdGeom.Cylinder prim.""" cfg = sim_utils.CylinderCfg(radius=1.0, height=2.0, axis="Y") prim = cfg.func("/World/Cylinder", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cylinder") + assert sim.stage.GetPrimAtPath("/World/Cylinder").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Cylinder/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cylinder/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Cylinder" assert prim.GetAttribute("radius").Get() == cfg.radius assert prim.GetAttribute("height").Get() == cfg.height @@ -89,12 +92,13 @@ def test_spawn_cuboid(sim): """Test spawning of UsdGeom.Cube prim.""" cfg = sim_utils.CuboidCfg(size=(1.0, 2.0, 3.0)) prim = cfg.func("/World/Cube", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cube") + assert sim.stage.GetPrimAtPath("/World/Cube").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Cube/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cube/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Cube" assert prim.GetAttribute("size").Get() == min(cfg.size) @@ -103,12 +107,13 @@ def test_spawn_sphere(sim): """Test spawning of UsdGeom.Sphere prim.""" cfg = sim_utils.SphereCfg(radius=1.0) prim = cfg.func("/World/Sphere", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Sphere") + assert sim.stage.GetPrimAtPath("/World/Sphere").IsValid() assert prim.GetPrimTypeInfo().GetTypeName() == "Xform" # Check properties - prim = prim_utils.get_prim_at_path("/World/Sphere/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Sphere/geometry/mesh") assert prim.GetPrimTypeInfo().GetTypeName() == "Sphere" assert prim.GetAttribute("radius").Get() == cfg.radius @@ -133,11 +138,12 @@ def test_spawn_cone_with_rigid_props(sim): ), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() # Check properties - prim = prim_utils.get_prim_at_path("/World/Cone") + prim = sim.stage.GetPrimAtPath("/World/Cone") assert prim.GetAttribute("physics:rigidBodyEnabled").Get() == cfg.rigid_props.rigid_body_enabled assert ( prim.GetAttribute("physxRigidBody:solverPositionIterationCount").Get() @@ -157,11 +163,12 @@ def test_spawn_cone_with_rigid_and_mass_props(sim): mass_props=sim_utils.MassPropertiesCfg(mass=1.0), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() # Check properties - prim = prim_utils.get_prim_at_path("/World/Cone") + prim = sim.stage.GetPrimAtPath("/World/Cone") assert prim.GetAttribute("physics:mass").Get() == cfg.mass_props.mass # check sim playing @@ -188,11 +195,12 @@ def test_spawn_cone_with_rigid_and_density_props(sim): collision_props=sim_utils.CollisionPropertiesCfg(collision_enabled=False), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() # Check properties - prim = prim_utils.get_prim_at_path("/World/Cone") + prim = sim.stage.GetPrimAtPath("/World/Cone") assert prim.GetAttribute("physics:density").Get() == cfg.mass_props.density # check sim playing @@ -213,16 +221,17 @@ def test_spawn_cone_with_all_props(sim): physics_material=sim_utils.materials.RigidBodyMaterialCfg(), ) prim = cfg.func("/World/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.is_prim_path_valid("/World/Cone") - assert prim_utils.is_prim_path_valid("/World/Cone/geometry/material") + assert sim.stage.GetPrimAtPath("/World/Cone").IsValid() + assert sim.stage.GetPrimAtPath("/World/Cone/geometry/material").IsValid() # Check properties # -- rigid body properties - prim = prim_utils.get_prim_at_path("/World/Cone") + prim = sim.stage.GetPrimAtPath("/World/Cone") assert prim.GetAttribute("physics:rigidBodyEnabled").Get() is True # -- collision properties - prim = prim_utils.get_prim_at_path("/World/Cone/geometry/mesh") + prim = sim.stage.GetPrimAtPath("/World/Cone/geometry/mesh") assert prim.GetAttribute("physics:collisionEnabled").Get() is True # check sim playing @@ -240,7 +249,7 @@ def test_spawn_cone_clones_invalid_paths(sim): """Test spawning of cone clones on invalid cloning paths.""" num_clones = 10 for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) # Spawn cone on invalid cloning path -- should raise an error cfg = sim_utils.ConeCfg(radius=1.0, height=2.0, copy_from_source=True) with pytest.raises(RuntimeError): @@ -251,15 +260,16 @@ def test_spawn_cone_clones(sim): """Test spawning of cone clones.""" num_clones = 10 for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) # Spawn cone on valid cloning path cfg = sim_utils.ConeCfg(radius=1.0, height=2.0, copy_from_source=True) prim = cfg.func("/World/env_.*/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.get_prim_path(prim) == "/World/env_0/Cone" + assert str(prim.GetPath()) == "/World/env_0/Cone" # find matching prims - prims = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + prims = sim_utils.find_matching_prim_paths("/World/env_.*/Cone") assert len(prims) == num_clones @@ -267,7 +277,7 @@ def test_spawn_cone_clone_with_all_props_global_material(sim): """Test spawning of cone clones with global material reference.""" num_clones = 10 for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) # Spawn cone on valid cloning path cfg = sim_utils.ConeCfg( radius=1.0, @@ -281,12 +291,13 @@ def test_spawn_cone_clone_with_all_props_global_material(sim): physics_material_path="/Looks/physicsMaterial", ) prim = cfg.func("/World/env_.*/Cone", cfg) + # Check validity assert prim.IsValid() - assert prim_utils.get_prim_path(prim) == "/World/env_0/Cone" + assert str(prim.GetPath()) == "/World/env_0/Cone" # find matching prims - prims = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + prims = sim_utils.find_matching_prim_paths("/World/env_.*/Cone") assert len(prims) == num_clones # find matching material prims - prims = prim_utils.find_matching_prim_paths("/Looks/visualMaterial.*") + prims = sim_utils.find_matching_prim_paths("/Looks/visualMaterial.*") assert len(prims) == 1 diff --git a/source/isaaclab/test/sim/test_spawn_wrappers.py b/source/isaaclab/test/sim/test_spawn_wrappers.py index 5edae7a79d6c..1571bb62bdc4 100644 --- a/source/isaaclab/test/sim/test_spawn_wrappers.py +++ b/source/isaaclab/test/sim/test_spawn_wrappers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,22 +12,21 @@ """Rest everything follows.""" -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils + import pytest -from isaacsim.core.api.simulation_context import SimulationContext import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR @pytest.fixture def sim(): """Create a simulation context.""" - stage_utils.create_new_stage() + sim_utils.create_new_stage() dt = 0.1 - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, backend="numpy") - stage_utils.update_stage() + sim = SimulationContext(SimulationCfg(dt=dt)) + sim_utils.update_stage() yield sim sim.stop() sim.clear() @@ -39,7 +38,7 @@ def test_spawn_multiple_shapes_with_global_settings(sim): """Test spawning of shapes randomly with global rigid body settings.""" num_clones = 10 for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) cfg = sim_utils.MultiAssetSpawnerCfg( assets_cfg=[ @@ -68,12 +67,12 @@ def test_spawn_multiple_shapes_with_global_settings(sim): prim = cfg.func("/World/env_.*/Cone", cfg) assert prim.IsValid() - assert prim_utils.get_prim_path(prim) == "/World/env_0/Cone" - prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + assert str(prim.GetPath()) == "/World/env_0/Cone" + prim_paths = sim_utils.find_matching_prim_paths("/World/env_.*/Cone") assert len(prim_paths) == num_clones for prim_path in prim_paths: - prim = prim_utils.get_prim_at_path(prim_path) + prim = sim.stage.GetPrimAtPath(prim_path) assert prim.GetAttribute("physics:mass").Get() == cfg.mass_props.mass @@ -81,7 +80,7 @@ def test_spawn_multiple_shapes_with_individual_settings(sim): """Test spawning of shapes randomly with individual rigid object settings.""" num_clones = 10 for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) mass_variations = [2.0, 3.0, 4.0] cfg = sim_utils.MultiAssetSpawnerCfg( @@ -114,12 +113,12 @@ def test_spawn_multiple_shapes_with_individual_settings(sim): prim = cfg.func("/World/env_.*/Cone", cfg) assert prim.IsValid() - assert prim_utils.get_prim_path(prim) == "/World/env_0/Cone" - prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + assert str(prim.GetPath()) == "/World/env_0/Cone" + prim_paths = sim_utils.find_matching_prim_paths("/World/env_.*/Cone") assert len(prim_paths) == num_clones for prim_path in prim_paths: - prim = prim_utils.get_prim_at_path(prim_path) + prim = sim.stage.GetPrimAtPath(prim_path) assert prim.GetAttribute("physics:mass").Get() in mass_variations @@ -132,7 +131,7 @@ def test_spawn_multiple_files_with_global_settings(sim): """Test spawning of files randomly with global articulation settings.""" num_clones = 10 for i in range(num_clones): - prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) cfg = sim_utils.MultiUsdFileCfg( usd_path=[ @@ -157,6 +156,6 @@ def test_spawn_multiple_files_with_global_settings(sim): prim = cfg.func("/World/env_.*/Robot", cfg) assert prim.IsValid() - assert prim_utils.get_prim_path(prim) == "/World/env_0/Robot" - prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Robot") + assert str(prim.GetPath()) == "/World/env_0/Robot" + prim_paths = sim_utils.find_matching_prim_paths("/World/env_.*/Robot") assert len(prim_paths) == num_clones diff --git a/source/isaaclab/test/sim/test_urdf_converter.py b/source/isaaclab/test/sim/test_urdf_converter.py index f238fd02408e..f91c58f015f9 100644 --- a/source/isaaclab/test/sim/test_urdf_converter.py +++ b/source/isaaclab/test/sim/test_urdf_converter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,27 +12,36 @@ """Rest everything follows.""" -import numpy as np import os -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils +import numpy as np import pytest -from isaacsim.core.api.simulation_context import SimulationContext +from packaging.version import Version + +import omni.kit.app from isaacsim.core.prims import Articulation -from isaacsim.core.utils.extensions import enable_extension, get_extension_path_from_name +import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.sim.converters import UrdfConverter, UrdfConverterCfg +from isaaclab.utils.version import get_isaac_sim_version # Create a fixture for setup and teardown @pytest.fixture def sim_config(): # Create a new stage - stage_utils.create_new_stage() - # retrieve path to urdf importer extension - enable_extension("isaacsim.asset.importer.urdf") - extension_path = get_extension_path_from_name("isaacsim.asset.importer.urdf") + sim_utils.create_new_stage() + # pin the urdf importer extension to the older version + manager = omni.kit.app.get_app().get_extension_manager() + if get_isaac_sim_version() >= Version("5.1"): + pinned_urdf_extension_name = "isaacsim.asset.importer.urdf-2.4.31" + manager.set_extension_enabled_immediate(pinned_urdf_extension_name, True) + else: + pinned_urdf_extension_name = "isaacsim.asset.importer.urdf" + # obtain the extension path + extension_id = manager.get_enabled_extension_id(pinned_urdf_extension_name) + extension_path = manager.get_extension_path(extension_id) # default configuration config = UrdfConverterCfg( asset_path=f"{extension_path}/data/urdf/robots/franka_description/robots/panda_arm_hand.urdf", @@ -44,9 +53,10 @@ def sim_config(): # Simulation time-step dt = 0.01 # Load kit helper - sim = SimulationContext(physics_dt=dt, rendering_dt=dt, stage_units_in_meters=1.0, backend="numpy") + sim = SimulationContext(SimulationCfg(dt=dt)) yield sim, config # Teardown + sim._disable_app_control_on_stop_handle = True # prevent timeout sim.stop() sim.clear() sim.clear_all_callbacks() @@ -96,9 +106,9 @@ def test_create_prim_from_usd(sim_config): urdf_converter = UrdfConverter(config) prim_path = "/World/Robot" - prim_utils.create_prim(prim_path, usd_path=urdf_converter.usd_path) + sim_utils.create_prim(prim_path, usd_path=urdf_converter.usd_path) - assert prim_utils.is_prim_path_valid(prim_path) + assert sim.stage.GetPrimAtPath(prim_path).IsValid() @pytest.mark.isaacsim_ci @@ -120,7 +130,7 @@ def test_config_drive_type(sim_config): urdf_converter = UrdfConverter(config) # check the drive type of the robot prim_path = "/World/Robot" - prim_utils.create_prim(prim_path, usd_path=urdf_converter.usd_path) + sim_utils.create_prim(prim_path, usd_path=urdf_converter.usd_path) # access the robot robot = Articulation(prim_path, reset_xform_properties=False) @@ -130,11 +140,13 @@ def test_config_drive_type(sim_config): # check drive values for the robot (read from physx) drive_stiffness, drive_damping = robot.get_gains() - np.testing.assert_array_equal(drive_stiffness, config.joint_drive.gains.stiffness) - np.testing.assert_array_equal(drive_damping, config.joint_drive.gains.damping) + np.testing.assert_array_equal(drive_stiffness.cpu().numpy(), config.joint_drive.gains.stiffness) + np.testing.assert_array_equal(drive_damping.cpu().numpy(), config.joint_drive.gains.damping) # check drive values for the robot (read from usd) + # Note: Disable the app control callback to prevent hanging during sim.stop() + sim._disable_app_control_on_stop_handle = True sim.stop() drive_stiffness, drive_damping = robot.get_gains() - np.testing.assert_array_equal(drive_stiffness, config.joint_drive.gains.stiffness) - np.testing.assert_array_equal(drive_damping, config.joint_drive.gains.damping) + np.testing.assert_array_equal(drive_stiffness.cpu().numpy(), config.joint_drive.gains.stiffness) + np.testing.assert_array_equal(drive_damping.cpu().numpy(), config.joint_drive.gains.damping) diff --git a/source/isaaclab/test/sim/test_utils.py b/source/isaaclab/test/sim/test_utils.py deleted file mode 100644 index a18e05342941..000000000000 --- a/source/isaaclab/test/sim/test_utils.py +++ /dev/null @@ -1,341 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Launch Isaac Sim Simulator first.""" - -from isaaclab.app import AppLauncher - -# launch omniverse app -simulation_app = AppLauncher(headless=True).app - -"""Rest everything follows.""" - -import numpy as np -import torch - -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils -import pytest -from pxr import Sdf, Usd, UsdGeom, UsdPhysics - -import isaaclab.sim as sim_utils -import isaaclab.utils.math as math_utils -from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR - - -@pytest.fixture(autouse=True) -def test_setup_teardown(): - """Create a blank new stage for each test.""" - # Setup: Create a new stage - stage_utils.create_new_stage() - stage_utils.update_stage() - - # Yield for the test - yield - - # Teardown: Clear stage after each test - stage_utils.clear_stage() - - -def test_get_all_matching_child_prims(): - """Test get_all_matching_child_prims() function.""" - # create scene - prim_utils.create_prim("/World/Floor") - prim_utils.create_prim("/World/Floor/Box", "Cube", position=np.array([75, 75, -150.1]), attributes={"size": 300}) - prim_utils.create_prim("/World/Wall", "Sphere", attributes={"radius": 1e3}) - - # test - isaac_sim_result = prim_utils.get_all_matching_child_prims("/World") - isaaclab_result = sim_utils.get_all_matching_child_prims("/World") - assert isaac_sim_result == isaaclab_result - - # add articulation root prim -- this asset has instanced prims - # note: isaac sim function does not support instanced prims so we add it here - # after the above test for the above test to still pass. - prim_utils.create_prim( - "/World/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" - ) - - # test with predicate - isaaclab_result = sim_utils.get_all_matching_child_prims("/World", predicate=lambda x: x.GetTypeName() == "Cube") - assert len(isaaclab_result) == 1 - assert isaaclab_result[0].GetPrimPath() == "/World/Floor/Box" - - # test with predicate and instanced prims - isaaclab_result = sim_utils.get_all_matching_child_prims( - "/World/Franka/panda_hand/visuals", predicate=lambda x: x.GetTypeName() == "Mesh" - ) - assert len(isaaclab_result) == 1 - assert isaaclab_result[0].GetPrimPath() == "/World/Franka/panda_hand/visuals/panda_hand" - - # test valid path - with pytest.raises(ValueError): - sim_utils.get_all_matching_child_prims("World/Room") - - -def test_get_first_matching_child_prim(): - """Test get_first_matching_child_prim() function.""" - # create scene - prim_utils.create_prim("/World/Floor") - prim_utils.create_prim( - "/World/env_1/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" - ) - prim_utils.create_prim( - "/World/env_2/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" - ) - prim_utils.create_prim( - "/World/env_0/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" - ) - - # test - isaaclab_result = sim_utils.get_first_matching_child_prim( - "/World", predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) - ) - assert isaaclab_result is not None - assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka" - - # test with instanced prims - isaaclab_result = sim_utils.get_first_matching_child_prim( - "/World/env_1/Franka", predicate=lambda prim: prim.GetTypeName() == "Mesh" - ) - assert isaaclab_result is not None - assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka/panda_link0/visuals/panda_link0" - - -def test_find_matching_prim_paths(): - """Test find_matching_prim_paths() function.""" - # create scene - for index in range(2048): - random_pos = np.random.uniform(-100, 100, size=3) - prim_utils.create_prim(f"/World/Floor_{index}", "Cube", position=random_pos, attributes={"size": 2.0}) - prim_utils.create_prim(f"/World/Floor_{index}/Sphere", "Sphere", attributes={"radius": 10}) - prim_utils.create_prim(f"/World/Floor_{index}/Sphere/childSphere", "Sphere", attributes={"radius": 1}) - prim_utils.create_prim(f"/World/Floor_{index}/Sphere/childSphere2", "Sphere", attributes={"radius": 1}) - - # test leaf paths - isaac_sim_result = prim_utils.find_matching_prim_paths("/World/Floor_.*/Sphere") - isaaclab_result = sim_utils.find_matching_prim_paths("/World/Floor_.*/Sphere") - assert isaac_sim_result == isaaclab_result - - # test non-leaf paths - isaac_sim_result = prim_utils.find_matching_prim_paths("/World/Floor_.*") - isaaclab_result = sim_utils.find_matching_prim_paths("/World/Floor_.*") - assert isaac_sim_result == isaaclab_result - - # test child-leaf paths - isaac_sim_result = prim_utils.find_matching_prim_paths("/World/Floor_.*/Sphere/childSphere.*") - isaaclab_result = sim_utils.find_matching_prim_paths("/World/Floor_.*/Sphere/childSphere.*") - assert isaac_sim_result == isaaclab_result - - # test valid path - with pytest.raises(ValueError): - sim_utils.get_all_matching_child_prims("World/Floor_.*") - - -def test_find_global_fixed_joint_prim(): - """Test find_global_fixed_joint_prim() function.""" - # create scene - prim_utils.create_prim("/World") - prim_utils.create_prim("/World/ANYmal", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd") - prim_utils.create_prim( - "/World/Franka", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" - ) - if "4.5" in ISAAC_NUCLEUS_DIR: - franka_usd = f"{ISAAC_NUCLEUS_DIR}/Robots/Franka/franka.usd" - else: - franka_usd = f"{ISAAC_NUCLEUS_DIR}/Robots/FrankaRobotics/FrankaPanda/franka.usd" - prim_utils.create_prim("/World/Franka_Isaac", usd_path=franka_usd) - - # test - assert sim_utils.find_global_fixed_joint_prim("/World/ANYmal") is None - assert sim_utils.find_global_fixed_joint_prim("/World/Franka") is not None - assert sim_utils.find_global_fixed_joint_prim("/World/Franka_Isaac") is not None - - # make fixed joint disabled manually - joint_prim = sim_utils.find_global_fixed_joint_prim("/World/Franka") - joint_prim.GetJointEnabledAttr().Set(False) - assert sim_utils.find_global_fixed_joint_prim("/World/Franka") is not None - assert sim_utils.find_global_fixed_joint_prim("/World/Franka", check_enabled_only=True) is None - - -def test_select_usd_variants(): - """Test select_usd_variants() function.""" - stage = stage_utils.get_current_stage() - prim: Usd.Prim = UsdGeom.Xform.Define(stage, Sdf.Path("/World")).GetPrim() - stage.SetDefaultPrim(prim) - - # Create the variant set and add your variants to it. - variants = ["red", "blue", "green"] - variant_set = prim.GetVariantSets().AddVariantSet("colors") - for variant in variants: - variant_set.AddVariant(variant) - - # Set the variant selection - sim_utils.utils.select_usd_variants("/World", {"colors": "red"}, stage) - - # Check if the variant selection is correct - assert variant_set.GetVariantSelection() == "red" - - -def test_resolve_prim_pose(): - """Test resolve_prim_pose() function.""" - # number of objects - num_objects = 20 - # sample random scales for x, y, z - rand_scales = np.random.uniform(0.5, 1.5, size=(num_objects, 3, 3)) - rand_widths = np.random.uniform(0.1, 10.0, size=(num_objects,)) - # sample random positions - rand_positions = np.random.uniform(-100, 100, size=(num_objects, 3, 3)) - # sample random rotations - rand_quats = np.random.randn(num_objects, 3, 4) - rand_quats /= np.linalg.norm(rand_quats, axis=2, keepdims=True) - - # create objects - for i in range(num_objects): - # simple cubes - cube_prim = prim_utils.create_prim( - f"/World/Cubes/instance_{i:02d}", - "Cube", - translation=rand_positions[i, 0], - orientation=rand_quats[i, 0], - scale=rand_scales[i, 0], - attributes={"size": rand_widths[i]}, - ) - # xform hierarchy - xform_prim = prim_utils.create_prim( - f"/World/Xform/instance_{i:02d}", - "Xform", - translation=rand_positions[i, 1], - orientation=rand_quats[i, 1], - scale=rand_scales[i, 1], - ) - geometry_prim = prim_utils.create_prim( - f"/World/Xform/instance_{i:02d}/geometry", - "Sphere", - translation=rand_positions[i, 2], - orientation=rand_quats[i, 2], - scale=rand_scales[i, 2], - attributes={"radius": rand_widths[i]}, - ) - dummy_prim = prim_utils.create_prim( - f"/World/Xform/instance_{i:02d}/dummy", - "Sphere", - ) - - # cube prim w.r.t. world frame - pos, quat = sim_utils.resolve_prim_pose(cube_prim) - pos, quat = np.array(pos), np.array(quat) - quat = quat if np.sign(rand_quats[i, 0, 0]) == np.sign(quat[0]) else -quat - np.testing.assert_allclose(pos, rand_positions[i, 0], atol=1e-3) - np.testing.assert_allclose(quat, rand_quats[i, 0], atol=1e-3) - # xform prim w.r.t. world frame - pos, quat = sim_utils.resolve_prim_pose(xform_prim) - pos, quat = np.array(pos), np.array(quat) - quat = quat if np.sign(rand_quats[i, 1, 0]) == np.sign(quat[0]) else -quat - np.testing.assert_allclose(pos, rand_positions[i, 1], atol=1e-3) - np.testing.assert_allclose(quat, rand_quats[i, 1], atol=1e-3) - # dummy prim w.r.t. world frame - pos, quat = sim_utils.resolve_prim_pose(dummy_prim) - pos, quat = np.array(pos), np.array(quat) - quat = quat if np.sign(rand_quats[i, 1, 0]) == np.sign(quat[0]) else -quat - np.testing.assert_allclose(pos, rand_positions[i, 1], atol=1e-3) - np.testing.assert_allclose(quat, rand_quats[i, 1], atol=1e-3) - - # geometry prim w.r.t. xform prim - pos, quat = sim_utils.resolve_prim_pose(geometry_prim, ref_prim=xform_prim) - pos, quat = np.array(pos), np.array(quat) - quat = quat if np.sign(rand_quats[i, 2, 0]) == np.sign(quat[0]) else -quat - np.testing.assert_allclose(pos, rand_positions[i, 2] * rand_scales[i, 1], atol=1e-3) - # TODO: Enabling scale causes the test to fail because the current implementation of - # resolve_prim_pose does not correctly handle non-identity scales on Xform prims. This is a known - # limitation. Until this is fixed, the test is disabled here to ensure the test passes. - np.testing.assert_allclose(quat, rand_quats[i, 2], atol=1e-3) - - # dummy prim w.r.t. xform prim - pos, quat = sim_utils.resolve_prim_pose(dummy_prim, ref_prim=xform_prim) - pos, quat = np.array(pos), np.array(quat) - np.testing.assert_allclose(pos, np.zeros(3), atol=1e-3) - np.testing.assert_allclose(quat, np.array([1, 0, 0, 0]), atol=1e-3) - # xform prim w.r.t. cube prim - pos, quat = sim_utils.resolve_prim_pose(xform_prim, ref_prim=cube_prim) - pos, quat = np.array(pos), np.array(quat) - # -- compute ground truth values - gt_pos, gt_quat = math_utils.subtract_frame_transforms( - torch.from_numpy(rand_positions[i, 0]).unsqueeze(0), - torch.from_numpy(rand_quats[i, 0]).unsqueeze(0), - torch.from_numpy(rand_positions[i, 1]).unsqueeze(0), - torch.from_numpy(rand_quats[i, 1]).unsqueeze(0), - ) - gt_pos, gt_quat = gt_pos.squeeze(0).numpy(), gt_quat.squeeze(0).numpy() - quat = quat if np.sign(gt_quat[0]) == np.sign(quat[0]) else -quat - np.testing.assert_allclose(pos, gt_pos, atol=1e-3) - np.testing.assert_allclose(quat, gt_quat, atol=1e-3) - - -def test_resolve_prim_scale(): - """Test resolve_prim_scale() function. - - To simplify the test, we assume that the effective scale at a prim - is the product of the scales of the prims in the hierarchy: - - scale = scale_of_xform * scale_of_geometry_prim - - This is only true when rotations are identity or the transforms are - orthogonal and uniformly scaled. Otherwise, scale is not composable - like that in local component-wise fashion. - """ - # number of objects - num_objects = 20 - # sample random scales for x, y, z - rand_scales = np.random.uniform(0.5, 1.5, size=(num_objects, 3, 3)) - rand_widths = np.random.uniform(0.1, 10.0, size=(num_objects,)) - # sample random positions - rand_positions = np.random.uniform(-100, 100, size=(num_objects, 3, 3)) - - # create objects - for i in range(num_objects): - # simple cubes - cube_prim = prim_utils.create_prim( - f"/World/Cubes/instance_{i:02d}", - "Cube", - translation=rand_positions[i, 0], - scale=rand_scales[i, 0], - attributes={"size": rand_widths[i]}, - ) - # xform hierarchy - xform_prim = prim_utils.create_prim( - f"/World/Xform/instance_{i:02d}", - "Xform", - translation=rand_positions[i, 1], - scale=rand_scales[i, 1], - ) - geometry_prim = prim_utils.create_prim( - f"/World/Xform/instance_{i:02d}/geometry", - "Sphere", - translation=rand_positions[i, 2], - scale=rand_scales[i, 2], - attributes={"radius": rand_widths[i]}, - ) - dummy_prim = prim_utils.create_prim( - f"/World/Xform/instance_{i:02d}/dummy", - "Sphere", - ) - - # cube prim - scale = sim_utils.resolve_prim_scale(cube_prim) - scale = np.array(scale) - np.testing.assert_allclose(scale, rand_scales[i, 0], atol=1e-5) - # xform prim - scale = sim_utils.resolve_prim_scale(xform_prim) - scale = np.array(scale) - np.testing.assert_allclose(scale, rand_scales[i, 1], atol=1e-5) - # geometry prim - scale = sim_utils.resolve_prim_scale(geometry_prim) - scale = np.array(scale) - np.testing.assert_allclose(scale, rand_scales[i, 1] * rand_scales[i, 2], atol=1e-5) - # dummy prim - scale = sim_utils.resolve_prim_scale(dummy_prim) - scale = np.array(scale) - np.testing.assert_allclose(scale, rand_scales[i, 1], atol=1e-5) diff --git a/source/isaaclab/test/sim/test_utils_prims.py b/source/isaaclab/test/sim/test_utils_prims.py new file mode 100644 index 000000000000..16584d113ed7 --- /dev/null +++ b/source/isaaclab/test/sim/test_utils_prims.py @@ -0,0 +1,716 @@ +# 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 + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +# note: need to enable cameras to be able to make replicator core available +simulation_app = AppLauncher(headless=True, enable_cameras=True).app + +"""Rest everything follows.""" + +import math + +import numpy as np +import pytest +import torch + +from pxr import Gf, Sdf, Usd, UsdGeom + +import isaaclab.sim as sim_utils +from isaaclab.sim.utils.prims import _to_tuple # type: ignore[reportPrivateUsage] +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR + + +@pytest.fixture(autouse=True) +def test_setup_teardown(): + """Create a blank new stage for each test.""" + # Setup: Create a new stage + sim_utils.create_new_stage() + sim_utils.update_stage() + + # Yield for the test + yield + + # Teardown: Clear stage after each test + sim_utils.clear_stage() + + +def assert_quat_close(q1: Gf.Quatf | Gf.Quatd, q2: Gf.Quatf | Gf.Quatd, eps: float = 1e-6): + """Assert two quaternions are close.""" + assert math.isclose(q1.GetReal(), q2.GetReal(), abs_tol=eps) + for i in range(3): + assert math.isclose(q1.GetImaginary()[i], q2.GetImaginary()[i], abs_tol=eps) + + +""" +General Utils +""" + + +def test_create_prim(): + """Test create_prim() function.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create scene + prim = sim_utils.create_prim(prim_path="/World/Test", prim_type="Xform", stage=stage) + # check prim created + assert prim.IsValid() + assert prim.GetPrimPath() == "/World/Test" + assert prim.GetTypeName() == "Xform" + + # check recreation of prim + with pytest.raises(ValueError, match="already exists"): + sim_utils.create_prim(prim_path="/World/Test", prim_type="Xform", stage=stage) + + # check attribute setting + prim = sim_utils.create_prim(prim_path="/World/Test/Cube", prim_type="Cube", stage=stage, attributes={"size": 100}) + # check attribute set + assert prim.IsValid() + assert prim.GetPrimPath() == "/World/Test/Cube" + assert prim.GetTypeName() == "Cube" + assert prim.GetAttribute("size").Get() == 100 + + # check adding USD reference + franka_usd = f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + prim = sim_utils.create_prim("/World/Test/USDReference", usd_path=franka_usd, stage=stage) + # check USD reference set + assert prim.IsValid() + assert prim.GetPrimPath() == "/World/Test/USDReference" + assert prim.GetTypeName() == "Xform" + # get the reference of the prim + references = [] + for prim_spec in prim.GetPrimStack(): + references.extend(prim_spec.referenceList.prependedItems) + assert len(references) == 1 + assert str(references[0].assetPath) == franka_usd + + # check adding semantic label + prim = sim_utils.create_prim( + "/World/Test/Sphere", "Sphere", stage=stage, semantic_label="sphere", attributes={"radius": 10.0} + ) + # check semantic label set + assert prim.IsValid() + assert prim.GetPrimPath() == "/World/Test/Sphere" + assert prim.GetTypeName() == "Sphere" + assert prim.GetAttribute("radius").Get() == 10.0 + assert sim_utils.get_labels(prim)["class"] == ["sphere"] + + # check setting transform + pos = (1.0, 2.0, 3.0) + quat = (0.0, 0.0, 0.0, 1.0) + scale = (1.0, 0.5, 0.5) + prim = sim_utils.create_prim( + "/World/Test/Xform", "Xform", stage=stage, translation=pos, orientation=quat, scale=scale + ) + # check transform set + assert prim.IsValid() + assert prim.GetPrimPath() == "/World/Test/Xform" + assert prim.GetTypeName() == "Xform" + assert prim.GetAttribute("xformOp:translate").Get() == Gf.Vec3d(pos) + assert_quat_close(prim.GetAttribute("xformOp:orient").Get(), Gf.Quatd(*quat)) + assert prim.GetAttribute("xformOp:scale").Get() == Gf.Vec3d(scale) + # check xform operation order + op_names = [op.GetOpName() for op in UsdGeom.Xformable(prim).GetOrderedXformOps()] + assert op_names == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + +@pytest.mark.parametrize( + "input_type", + ["list", "tuple", "numpy", "torch_cpu", "torch_cuda"], + ids=["list", "tuple", "numpy", "torch_cpu", "torch_cuda"], +) +def test_create_prim_with_different_input_types(input_type: str): + """Test create_prim() with different input types (list, tuple, numpy array, torch tensor).""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Define test values + translation_vals = [1.0, 2.0, 3.0] + orientation_vals = [1.0, 0.0, 0.0, 0.0] # w, x, y, z + scale_vals = [2.0, 3.0, 4.0] + + # Convert to the specified input type + if input_type == "list": + translation = translation_vals + orientation = orientation_vals + scale = scale_vals + elif input_type == "tuple": + translation = tuple(translation_vals) + orientation = tuple(orientation_vals) + scale = tuple(scale_vals) + elif input_type == "numpy": + translation = np.array(translation_vals) + orientation = np.array(orientation_vals) + scale = np.array(scale_vals) + elif input_type == "torch_cpu": + translation = torch.tensor(translation_vals) + orientation = torch.tensor(orientation_vals) + scale = torch.tensor(scale_vals) + elif input_type == "torch_cuda": + if not torch.cuda.is_available(): + pytest.skip("CUDA not available") + translation = torch.tensor(translation_vals, device="cuda") + orientation = torch.tensor(orientation_vals, device="cuda") + scale = torch.tensor(scale_vals, device="cuda") + + # Create prim with translation (local space) + prim = sim_utils.create_prim( + f"/World/Test/Xform_{input_type}", + "Xform", + stage=stage, + translation=translation, + orientation=orientation, + scale=scale, + ) + + # Verify prim was created correctly + assert prim.IsValid() + assert prim.GetPrimPath() == f"/World/Test/Xform_{input_type}" + + # Verify transform values + assert prim.GetAttribute("xformOp:translate").Get() == Gf.Vec3d(*translation_vals) + assert_quat_close(prim.GetAttribute("xformOp:orient").Get(), Gf.Quatd(*orientation_vals)) + assert prim.GetAttribute("xformOp:scale").Get() == Gf.Vec3d(*scale_vals) + + # Verify xform operation order + op_names = [op.GetOpName() for op in UsdGeom.Xformable(prim).GetOrderedXformOps()] + assert op_names == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + +@pytest.mark.parametrize( + "input_type", + ["list", "tuple", "numpy", "torch_cpu", "torch_cuda"], + ids=["list", "tuple", "numpy", "torch_cpu", "torch_cuda"], +) +def test_create_prim_with_world_position_different_types(input_type: str): + """Test create_prim() with world position using different input types.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a parent prim + _ = sim_utils.create_prim( + "/World/Parent", + "Xform", + stage=stage, + translation=(5.0, 10.0, 15.0), + orientation=(1.0, 0.0, 0.0, 0.0), + ) + + # Define world position and orientation values + world_pos_vals = [10.0, 20.0, 30.0] + world_orient_vals = [0.7071068, 0.0, 0.7071068, 0.0] # 90 deg around Y + + # Convert to the specified input type + if input_type == "list": + world_pos = world_pos_vals + world_orient = world_orient_vals + elif input_type == "tuple": + world_pos = tuple(world_pos_vals) + world_orient = tuple(world_orient_vals) + elif input_type == "numpy": + world_pos = np.array(world_pos_vals) + world_orient = np.array(world_orient_vals) + elif input_type == "torch_cpu": + world_pos = torch.tensor(world_pos_vals) + world_orient = torch.tensor(world_orient_vals) + elif input_type == "torch_cuda": + if not torch.cuda.is_available(): + pytest.skip("CUDA not available") + world_pos = torch.tensor(world_pos_vals, device="cuda") + world_orient = torch.tensor(world_orient_vals, device="cuda") + + # Create child prim with world position + child = sim_utils.create_prim( + f"/World/Parent/Child_{input_type}", + "Xform", + stage=stage, + position=world_pos, # Using position (world space) + orientation=world_orient, + ) + + # Verify prim was created + assert child.IsValid() + + # Verify world pose matches what we specified + world_pose = sim_utils.resolve_prim_pose(child) + pos_result, quat_result = world_pose + + # Check position (should be close to world_pos_vals) + for i in range(3): + assert math.isclose(pos_result[i], world_pos_vals[i], abs_tol=1e-4) + + # Check orientation (quaternions may have sign flipped) + quat_match = all(math.isclose(quat_result[i], world_orient_vals[i], abs_tol=1e-4) for i in range(4)) + quat_match_neg = all(math.isclose(quat_result[i], -world_orient_vals[i], abs_tol=1e-4) for i in range(4)) + assert quat_match or quat_match_neg + + +def test_create_prim_non_xformable(): + """Test create_prim() with non-Xformable prim types (Material, Shader, Scope). + + This test verifies that prims which are not Xformable (like Material, Shader, Scope) + are created successfully but transform operations are not applied to them. + This is expected behavior as documented in the create_prim function. + """ + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Test with Material prim (not Xformable) + material_prim = sim_utils.create_prim( + "/World/TestMaterial", + "Material", + stage=stage, + translation=(1.0, 2.0, 3.0), # These should be ignored + orientation=(1.0, 0.0, 0.0, 0.0), # These should be ignored + scale=(2.0, 2.0, 2.0), # These should be ignored + ) + + # Verify prim was created + assert material_prim.IsValid() + assert material_prim.GetPrimPath() == "/World/TestMaterial" + assert material_prim.GetTypeName() == "Material" + + # Verify that it's not Xformable + assert not material_prim.IsA(UsdGeom.Xformable) + + # Verify that no xform operations were applied (Material prims don't support these) + assert not material_prim.HasAttribute("xformOp:translate") + assert not material_prim.HasAttribute("xformOp:orient") + assert not material_prim.HasAttribute("xformOp:scale") + + # Test with Scope prim (not Xformable) + scope_prim = sim_utils.create_prim( + "/World/TestScope", + "Scope", + stage=stage, + translation=(5.0, 6.0, 7.0), # These should be ignored + ) + + # Verify prim was created + assert scope_prim.IsValid() + assert scope_prim.GetPrimPath() == "/World/TestScope" + assert scope_prim.GetTypeName() == "Scope" + + # Verify that it's not Xformable + assert not scope_prim.IsA(UsdGeom.Xformable) + + # Verify that no xform operations were applied (Scope prims don't support these) + assert not scope_prim.HasAttribute("xformOp:translate") + assert not scope_prim.HasAttribute("xformOp:orient") + assert not scope_prim.HasAttribute("xformOp:scale") + + +def test_delete_prim(): + """Test delete_prim() function.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create scene + prim = sim_utils.create_prim("/World/Test/Xform", "Xform", stage=stage) + # delete prim + sim_utils.delete_prim("/World/Test/Xform") + # check prim deleted + assert not prim.IsValid() + + # check for usd reference + prim = sim_utils.create_prim( + "/World/Test/USDReference", + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd", + stage=stage, + ) + # delete prim + sim_utils.delete_prim("/World/Test/USDReference", stage=stage) + # check prim deleted + assert not prim.IsValid() + + # check deleting multiple prims + prim1 = sim_utils.create_prim("/World/Test/Xform1", "Xform", stage=stage) + prim2 = sim_utils.create_prim("/World/Test/Xform2", "Xform", stage=stage) + sim_utils.delete_prim(("/World/Test/Xform1", "/World/Test/Xform2"), stage=stage) + # check prims deleted + assert not prim1.IsValid() + assert not prim2.IsValid() + + +def test_move_prim(): + """Test move_prim() function.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create scene + sim_utils.create_prim("/World/Test", "Xform", stage=stage) + prim = sim_utils.create_prim( + "/World/Test/Xform", + "Xform", + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd", + translation=(1.0, 2.0, 3.0), + orientation=(0.0, 0.0, 0.0, 1.0), + stage=stage, + ) + + # move prim + sim_utils.create_prim("/World/TestMove", "Xform", stage=stage, translation=(1.0, 1.0, 1.0)) + sim_utils.move_prim("/World/Test/Xform", "/World/TestMove/Xform", stage=stage) + # check prim moved + prim = stage.GetPrimAtPath("/World/TestMove/Xform") + assert prim.IsValid() + assert prim.GetPrimPath() == "/World/TestMove/Xform" + assert prim.GetAttribute("xformOp:translate").Get() == Gf.Vec3d((0.0, 1.0, 2.0)) + assert_quat_close(prim.GetAttribute("xformOp:orient").Get(), Gf.Quatd(0.0, 0.0, 0.0, 1.0)) + + # check moving prim with keep_world_transform=False + # it should preserve the local transform from last move + sim_utils.create_prim( + "/World/TestMove2", "Xform", stage=stage, translation=(2.0, 2.0, 2.0), orientation=(0.0, 0.7071, 0.0, 0.7071) + ) + sim_utils.move_prim("/World/TestMove/Xform", "/World/TestMove2/Xform", keep_world_transform=False, stage=stage) + # check prim moved + prim = stage.GetPrimAtPath("/World/TestMove2/Xform") + assert prim.IsValid() + assert prim.GetPrimPath() == "/World/TestMove2/Xform" + assert prim.GetAttribute("xformOp:translate").Get() == Gf.Vec3d((0.0, 1.0, 2.0)) + assert_quat_close(prim.GetAttribute("xformOp:orient").Get(), Gf.Quatd(0.0, 0.0, 0.0, 1.0)) + + +""" +USD references and variants. +""" + + +def test_get_usd_references(): + """Test get_usd_references() function.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim without USD reference + sim_utils.create_prim("/World/NoReference", "Xform", stage=stage) + # Check that it has no references + refs = sim_utils.get_usd_references("/World/NoReference", stage=stage) + assert len(refs) == 0 + + # Create a prim with a USD reference + franka_usd = f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + sim_utils.create_prim("/World/WithReference", usd_path=franka_usd, stage=stage) + # Check that it has the expected reference + refs = sim_utils.get_usd_references("/World/WithReference", stage=stage) + assert len(refs) == 1 + assert refs == [franka_usd] + + # Test with invalid prim path + with pytest.raises(ValueError, match="not valid"): + sim_utils.get_usd_references("/World/NonExistent", stage=stage) + + +def test_select_usd_variants(): + """Test select_usd_variants() function.""" + stage = sim_utils.get_current_stage() + + # Create a dummy prim + prim: Usd.Prim = UsdGeom.Xform.Define(stage, Sdf.Path("/World")).GetPrim() + stage.SetDefaultPrim(prim) + + # Create the variant set and add your variants to it. + variants = ["red", "blue", "green"] + variant_set = prim.GetVariantSets().AddVariantSet("colors") + for variant in variants: + variant_set.AddVariant(variant) + + # Set the variant selection + sim_utils.utils.select_usd_variants("/World", {"colors": "red"}, stage) + + # Check if the variant selection is correct + assert variant_set.GetVariantSelection() == "red" + + +def test_select_usd_variants_in_usd_file(): + """Test select_usd_variants() function in USD file.""" + stage = sim_utils.get_current_stage() + + prim = sim_utils.create_prim( + "/World/Test", "Xform", usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/UniversalRobots/ur10e/ur10e.usd", stage=stage + ) + + variant_sets = prim.GetVariantSets() + + # show all variants + for name in variant_sets.GetNames(): + vs = variant_sets.GetVariantSet(name) + options = vs.GetVariantNames() + selected = vs.GetVariantSelection() + + print(f"{name}: {selected} / {options}") + + print("Setting variant 'Gripper' to 'Robotiq_2f_140'.") + # The following performs the operations done internally + # in Isaac Lab. This should be removed in favor of 'select_usd_variants'. + target_vs = variant_sets.GetVariantSet("Gripper") + target_vs.SetVariantSelection("Robotiq_2f_140") + + # show again all variants + variant_sets = prim.GetVariantSets() + + for name in variant_sets.GetNames(): + vs = variant_sets.GetVariantSet(name) + options = vs.GetVariantNames() + selected = vs.GetVariantSelection() + + print(f"{name}: {selected} / {options}") + + # Uncomment the following once resolved + + # Set the variant selection + # sim_utils.select_usd_variants(prim.GetPath(), {"Gripper": "Robotiq_2f_140"}, stage) + + # Obtain variant set + # variant_set = prim.GetVariantSet("Gripper") + # # Check if the variant selection is correct + # assert variant_set.GetVariantSelection() == "Robotiq_2f_140" + + +""" +Property Management. +""" + + +def test_change_prim_property_basic(): + """Test change_prim_property() with existing property.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a cube prim + prim = sim_utils.create_prim("/World/Cube", "Cube", stage=stage, attributes={"size": 1.0}) + + # check initial value + assert prim.GetAttribute("size").Get() == 1.0 + + # change the property + result = sim_utils.change_prim_property( + prop_path="/World/Cube.size", + value=2.0, + stage=stage, + ) + + # check that the change was successful + assert result is True + assert prim.GetAttribute("size").Get() == 2.0 + + +def test_change_prim_property_create_new(): + """Test change_prim_property() creates new property when it doesn't exist.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a prim + prim = sim_utils.create_prim("/World/Test", "Xform", stage=stage) + + # check that the property doesn't exist + assert prim.GetAttribute("customValue").Get() is None + + # create a new property + result = sim_utils.change_prim_property( + prop_path="/World/Test.customValue", + value=42, + stage=stage, + type_to_create_if_not_exist=Sdf.ValueTypeNames.Int, + is_custom=True, + ) + + # check that the property was created successfully + assert result is True + assert prim.GetAttribute("customValue").Get() == 42 + + +def test_change_prim_property_clear_value(): + """Test change_prim_property() clears property value when value is None.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a cube with an attribute + prim = sim_utils.create_prim("/World/Cube", "Cube", stage=stage, attributes={"size": 1.0}) + + # check initial value + assert prim.GetAttribute("size").Get() == 1.0 + + # clear the property value + result = sim_utils.change_prim_property( + prop_path="/World/Cube.size", + value=None, + stage=stage, + ) + + # check that the value was cleared + assert result is True + # Note: After clearing, the attribute should go its default value + assert prim.GetAttribute("size").Get() == 2.0 + + +@pytest.mark.parametrize( + "attr_name,value,value_type,expected", + [ + ("floatValue", 3.14, Sdf.ValueTypeNames.Float, 3.14), + ("boolValue", True, Sdf.ValueTypeNames.Bool, True), + ("intValue", 42, Sdf.ValueTypeNames.Int, 42), + ("stringValue", "test", Sdf.ValueTypeNames.String, "test"), + ("vec3Value", Gf.Vec3f(1.0, 2.0, 3.0), Sdf.ValueTypeNames.Float3, Gf.Vec3f(1.0, 2.0, 3.0)), + ("colorValue", Gf.Vec3f(1.0, 0.0, 0.5), Sdf.ValueTypeNames.Color3f, Gf.Vec3f(1.0, 0.0, 0.5)), + ], + ids=["float", "bool", "int", "string", "vec3", "color"], +) +def test_change_prim_property_different_types(attr_name: str, value, value_type, expected): + """Test change_prim_property() with different value types.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a prim + prim = sim_utils.create_prim("/World/Test", "Xform", stage=stage) + + # change the property + result = sim_utils.change_prim_property( + prop_path=f"/World/Test.{attr_name}", + value=value, + stage=stage, + type_to_create_if_not_exist=value_type, + is_custom=True, + ) + + # check that the change was successful + assert result is True + actual_value = prim.GetAttribute(attr_name).Get() + + # handle float comparison separately for precision + if isinstance(expected, float): + assert math.isclose(actual_value, expected, abs_tol=1e-6) + else: + assert actual_value == expected + + +@pytest.mark.parametrize( + "prop_path_input", + ["/World/Cube.size", Sdf.Path("/World/Cube.size")], + ids=["str_path", "sdf_path"], +) +def test_change_prim_property_path_types(prop_path_input): + """Test change_prim_property() with different path input types.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a cube prim + prim = sim_utils.create_prim("/World/Cube", "Cube", stage=stage, attributes={"size": 1.0}) + + # change property using different path types + result = sim_utils.change_prim_property( + prop_path=prop_path_input, + value=3.0, + stage=stage, + ) + + # check that the change was successful + assert result is True + assert prim.GetAttribute("size").Get() == 3.0 + + +def test_change_prim_property_error_invalid_prim(): + """Test change_prim_property() raises error for invalid prim path.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # try to change property on non-existent prim + with pytest.raises(ValueError, match="Prim does not exist"): + sim_utils.change_prim_property( + prop_path="/World/NonExistent.property", + value=1.0, + stage=stage, + ) + + +def test_change_prim_property_error_missing_type(): + """Test change_prim_property() returns False when property doesn't exist and type not provided.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + # create a prim + prim = sim_utils.create_prim("/World/Test", "Xform", stage=stage) + + # try to create property without providing type + result = sim_utils.change_prim_property( + prop_path="/World/Test.nonExistentProperty", + value=42, + stage=stage, + ) + + # should return False since type was not provided + assert result is False + # property should not have been created + assert prim.GetAttribute("nonExistentProperty").Get() is None + + +""" +Internal Helpers. +""" + + +def test_to_tuple_basic(): + """Test _to_tuple() with basic input types.""" + # Test with list + result = _to_tuple([1.0, 2.0, 3.0]) + assert result == (1.0, 2.0, 3.0) + assert isinstance(result, tuple) + + # Test with tuple + result = _to_tuple((1.0, 2.0, 3.0)) + assert result == (1.0, 2.0, 3.0) + + # Test with numpy array + result = _to_tuple(np.array([1.0, 2.0, 3.0])) + assert result == (1.0, 2.0, 3.0) + + # Test with torch tensor (CPU) + result = _to_tuple(torch.tensor([1.0, 2.0, 3.0])) + assert result == (1.0, 2.0, 3.0) + + # Test squeezing first dimension (batch size 1) + result = _to_tuple(torch.tensor([[1.0, 2.0]])) + assert result == (1.0, 2.0) + + result = _to_tuple(np.array([[1.0, 2.0, 3.0]])) + assert result == (1.0, 2.0, 3.0) + + +def test_to_tuple_raises_error(): + """Test _to_tuple() raises an error for N-dimensional arrays.""" + + with pytest.raises(ValueError, match="not one dimensional"): + _to_tuple(np.array([[1.0, 2.0], [3.0, 4.0]])) + + with pytest.raises(ValueError, match="not one dimensional"): + _to_tuple(torch.tensor([[[1.0, 2.0]], [[3.0, 4.0]]])) + + with pytest.raises(ValueError, match="only one element tensors can be converted"): + _to_tuple((torch.tensor([1.0, 2.0]), 3.0)) + + +def test_to_tuple_mixed_sequences(): + """Test _to_tuple() with mixed type sequences.""" + + # Mixed list with numpy and floats + result = _to_tuple([np.float32(1.0), 2.0, 3.0]) + assert len(result) == 3 + assert all(isinstance(x, float) for x in result) + + # Mixed tuple with torch tensor items and floats + result = _to_tuple([torch.tensor(1.0), 2.0, 3.0]) + assert result == (1.0, 2.0, 3.0) + + # Mixed tuple with numpy array items and torch tensor + result = _to_tuple((np.float32(1.0), 2.0, torch.tensor(3.0))) + assert result == (1.0, 2.0, 3.0) + + +def test_to_tuple_precision(): + """Test _to_tuple() maintains numerical precision.""" + from isaaclab.sim.utils.prims import _to_tuple + + # Test with high precision values + high_precision = [1.123456789, 2.987654321, 3.141592653] + result = _to_tuple(torch.tensor(high_precision, dtype=torch.float64)) + + # Check that precision is maintained reasonably well + for i, val in enumerate(high_precision): + assert math.isclose(result[i], val, abs_tol=1e-6) diff --git a/source/isaaclab/test/sim/test_utils_queries.py b/source/isaaclab/test/sim/test_utils_queries.py new file mode 100644 index 000000000000..4f5a0758342c --- /dev/null +++ b/source/isaaclab/test/sim/test_utils_queries.py @@ -0,0 +1,171 @@ +# 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 + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +# note: need to enable cameras to be able to make replicator core available +simulation_app = AppLauncher(headless=True, enable_cameras=True).app + +"""Rest everything follows.""" + +import pytest + +from pxr import UsdPhysics + +import isaaclab.sim as sim_utils +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR + + +@pytest.fixture(autouse=True) +def test_setup_teardown(): + """Create a blank new stage for each test.""" + # Setup: Create a new stage + sim_utils.create_new_stage() + sim_utils.update_stage() + + # Yield for the test + yield + + # Teardown: Clear stage after each test + sim_utils.clear_stage() + + +""" +USD Stage Querying. +""" + + +def test_get_next_free_prim_path(): + """Test get_next_free_prim_path() function.""" + # create scene + sim_utils.create_prim("/World/Floor") + sim_utils.create_prim("/World/Floor/Box", "Cube", position=[75, 75, -150.1], attributes={"size": 300}) + sim_utils.create_prim("/World/Wall", "Sphere", attributes={"radius": 1e3}) + + # test + isaaclab_result = sim_utils.get_next_free_prim_path("/World/Floor") + assert isaaclab_result == "/World/Floor_01" + + # create another prim + sim_utils.create_prim("/World/Floor/Box_01", "Cube", position=[75, 75, -150.1], attributes={"size": 300}) + + # test again + isaaclab_result = sim_utils.get_next_free_prim_path("/World/Floor/Box") + assert isaaclab_result == "/World/Floor/Box_02" + + +def test_get_first_matching_ancestor_prim(): + """Test get_first_matching_ancestor_prim() function.""" + # create scene + sim_utils.create_prim("/World/Floor") + sim_utils.create_prim("/World/Floor/Box", "Cube", position=[75, 75, -150.1], attributes={"size": 300}) + sim_utils.create_prim("/World/Floor/Box/Sphere", "Sphere", attributes={"radius": 1e3}) + + # test with input prim not having the predicate + isaaclab_result = sim_utils.get_first_matching_ancestor_prim( + "/World/Floor/Box/Sphere", predicate=lambda x: x.GetTypeName() == "Cube" + ) + assert isaaclab_result is not None + assert isaaclab_result.GetPrimPath() == "/World/Floor/Box" + + # test with input prim having the predicate + isaaclab_result = sim_utils.get_first_matching_ancestor_prim( + "/World/Floor/Box", predicate=lambda x: x.GetTypeName() == "Cube" + ) + assert isaaclab_result is not None + assert isaaclab_result.GetPrimPath() == "/World/Floor/Box" + + # test with no predicate match + isaaclab_result = sim_utils.get_first_matching_ancestor_prim( + "/World/Floor/Box/Sphere", predicate=lambda x: x.GetTypeName() == "Cone" + ) + assert isaaclab_result is None + + +def test_get_all_matching_child_prims(): + """Test get_all_matching_child_prims() function.""" + # create scene + sim_utils.create_prim("/World/Floor") + sim_utils.create_prim("/World/Floor/Box", "Cube", position=[75, 75, -150.1], attributes={"size": 300}) + sim_utils.create_prim("/World/Wall", "Sphere", attributes={"radius": 1e3}) + + # add articulation root prim -- this asset has instanced prims + # note: isaac sim function does not support instanced prims so we add it here + # after the above test for the above test to still pass. + sim_utils.create_prim( + "/World/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + + # test with predicate + isaaclab_result = sim_utils.get_all_matching_child_prims("/World", predicate=lambda x: x.GetTypeName() == "Cube") + assert len(isaaclab_result) == 1 + assert isaaclab_result[0].GetPrimPath() == "/World/Floor/Box" + + # test with predicate and instanced prims + isaaclab_result = sim_utils.get_all_matching_child_prims( + "/World/Franka/panda_hand/visuals", predicate=lambda x: x.GetTypeName() == "Mesh" + ) + assert len(isaaclab_result) == 1 + assert isaaclab_result[0].GetPrimPath() == "/World/Franka/panda_hand/visuals/panda_hand" + + # test valid path + with pytest.raises(ValueError): + sim_utils.get_all_matching_child_prims("World/Room") + + +def test_get_first_matching_child_prim(): + """Test get_first_matching_child_prim() function.""" + # create scene + sim_utils.create_prim("/World/Floor") + sim_utils.create_prim( + "/World/env_1/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + sim_utils.create_prim( + "/World/env_2/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + sim_utils.create_prim( + "/World/env_0/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + + # test + isaaclab_result = sim_utils.get_first_matching_child_prim( + "/World", predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + ) + assert isaaclab_result is not None + assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka" + + # test with instanced prims + isaaclab_result = sim_utils.get_first_matching_child_prim( + "/World/env_1/Franka", predicate=lambda prim: prim.GetTypeName() == "Mesh" + ) + assert isaaclab_result is not None + assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka/panda_link0/visuals/panda_link0" + + +def test_find_global_fixed_joint_prim(): + """Test find_global_fixed_joint_prim() function.""" + # create scene + sim_utils.create_prim("/World") + sim_utils.create_prim("/World/ANYmal", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd") + sim_utils.create_prim("/World/Franka", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd") + if "4.5" in ISAAC_NUCLEUS_DIR: + franka_usd = f"{ISAAC_NUCLEUS_DIR}/Robots/Franka/franka.usd" + else: + franka_usd = f"{ISAAC_NUCLEUS_DIR}/Robots/FrankaRobotics/FrankaPanda/franka.usd" + sim_utils.create_prim("/World/Franka_Isaac", usd_path=franka_usd) + + # test + assert sim_utils.find_global_fixed_joint_prim("/World/ANYmal") is None + assert sim_utils.find_global_fixed_joint_prim("/World/Franka") is not None + assert sim_utils.find_global_fixed_joint_prim("/World/Franka_Isaac") is not None + + # make fixed joint disabled manually + joint_prim = sim_utils.find_global_fixed_joint_prim("/World/Franka") + joint_prim.GetJointEnabledAttr().Set(False) + assert sim_utils.find_global_fixed_joint_prim("/World/Franka") is not None + assert sim_utils.find_global_fixed_joint_prim("/World/Franka", check_enabled_only=True) is None diff --git a/source/isaaclab/test/sim/test_utils_semantics.py b/source/isaaclab/test/sim/test_utils_semantics.py new file mode 100644 index 000000000000..fe8cbd37187a --- /dev/null +++ b/source/isaaclab/test/sim/test_utils_semantics.py @@ -0,0 +1,231 @@ +# 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 + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +# note: need to enable cameras to be able to make replicator core available +simulation_app = AppLauncher(headless=True, enable_cameras=True).app + +"""Rest everything follows.""" + +import pytest + +import isaaclab.sim as sim_utils + + +@pytest.fixture(autouse=True) +def test_setup_teardown(): + """Create a blank new stage for each test.""" + # Setup: Create a new stage + sim_utils.create_new_stage() + sim_utils.update_stage() + + # Yield for the test + yield + + # Teardown: Clear stage after each test + sim_utils.clear_stage() + + +def create_test_environment_with_labels(): + """Creates a test environment with objects with labels.""" + # create 3 cubes with label "cube" + for i in range(3): + sim_utils.create_prim(f"/World/Test/Object{i}", "Cube", semantic_label="cube") + # create a sphere without any labels + sim_utils.create_prim("/World/Test/Object3", "Sphere") + # create a nested prim with label "nested" + nested_prim = sim_utils.create_prim("/World/Test/Object0/Nested", "Cube") + sim_utils.add_labels(nested_prim, ["nested"], instance_name="shape") + + return [f"/World/Test/Object{i}" for i in range(4)] + [str(nested_prim.GetPrimPath())] + + +""" +Tests. +""" + + +def test_add_and_get_labels(): + """Test add_labels() and get_labels() functions.""" + # get stage handle + stage = sim_utils.get_current_stage() + # create a test prim + prim = stage.DefinePrim("/test", "Xform") + nested_prim = stage.DefinePrim("/test/nested", "Xform") + + # Apply semantics + sim_utils.add_labels(prim, ["label_a", "label_b"], instance_name="class") + sim_utils.add_labels(prim, ["shape_a"], instance_name="shape") + sim_utils.add_labels(nested_prim, ["nested_label"], instance_name="class") + + # Get labels + labels_dict = sim_utils.get_labels(prim) + # Check labels are added correctly + assert "class" in labels_dict + assert sorted(labels_dict["class"]) == sorted(["label_a", "label_b"]) + assert "shape" in labels_dict + assert labels_dict["shape"] == ["shape_a"] + nested_labels_dict = sim_utils.get_labels(nested_prim) + assert "class" in nested_labels_dict + assert nested_labels_dict["class"] == ["nested_label"] + + +def test_add_labels_with_overwrite(): + """Test add_labels() function with overwriting existing labels.""" + # get stage handle + stage = sim_utils.get_current_stage() + # create a test prim + prim = stage.DefinePrim("/test", "Xform") + + # Add labels + sim_utils.add_labels(prim, ["label_a", "label_b"], instance_name="class") + sim_utils.add_labels(prim, ["shape_a"], instance_name="shape") + + # Overwrite existing labels for a specific instance + sim_utils.add_labels(prim, ["replaced_label"], instance_name="class", overwrite=True) + labels_dict = sim_utils.get_labels(prim) + assert labels_dict["class"] == ["replaced_label"] + assert "shape" in labels_dict + assert labels_dict["shape"] == ["shape_a"] + + +def test_add_labels_without_overwrite(): + """Test add_labels() function without overwriting existing labels.""" + # get stage handle + stage = sim_utils.get_current_stage() + # create a test prim + prim = stage.DefinePrim("/test", "Xform") + + # Add labels + sim_utils.add_labels(prim, ["label_a", "label_b"], instance_name="class") + sim_utils.add_labels(prim, ["shape_a"], instance_name="shape") + + # Re-add labels with overwrite=False (should append) + sim_utils.add_labels(prim, ["label_c"], instance_name="class", overwrite=False) + labels_dict = sim_utils.get_labels(prim) + assert sorted(labels_dict["class"]) == sorted(["label_a", "label_b", "label_c"]) + + +def test_remove_all_labels(): + """Test removing of all labels from a prim and its descendants.""" + # get stage handle + stage = sim_utils.get_current_stage() + # create a test prim + prim = stage.DefinePrim("/test", "Xform") + nested_prim = stage.DefinePrim("/test/nested", "Xform") + + # Add labels + sim_utils.add_labels(prim, ["label_a", "label_b"], instance_name="class") + sim_utils.add_labels(prim, ["shape_a"], instance_name="shape") + sim_utils.add_labels(nested_prim, ["nested_label"], instance_name="class") + + # Remove all labels + sim_utils.remove_labels(prim) + # Check labels are removed correctly + labels_dict = sim_utils.get_labels(prim) + assert len(labels_dict) == 0 + # Check nested prim labels are not removed + nested_labels_dict = sim_utils.get_labels(nested_prim) + assert "class" in nested_labels_dict + assert nested_labels_dict["class"] == ["nested_label"] + + # Re-add labels + sim_utils.add_labels(prim, ["label_a", "label_b"], instance_name="class") + sim_utils.add_labels(prim, ["shape_a"], instance_name="shape") + sim_utils.add_labels(nested_prim, ["nested_label"], instance_name="class") + # Remove all labels + sim_utils.remove_labels(prim, include_descendants=True) + # Check labels are removed correctly + labels_dict = sim_utils.get_labels(prim) + assert len(labels_dict) == 0 + # Check nested prim labels are removed + nested_labels_dict = sim_utils.get_labels(nested_prim) + assert len(nested_labels_dict) == 0 + + +def test_remove_specific_labels(): + """Test removing of specific labels from a prim and its descendants.""" + # get stage handle + stage = sim_utils.get_current_stage() + # create a test prim + prim = stage.DefinePrim("/test", "Xform") + nested_prim = stage.DefinePrim("/test/nested", "Xform") + + # Add labels + sim_utils.add_labels(prim, ["label_a", "label_b"], instance_name="class") + sim_utils.add_labels(prim, ["shape_a"], instance_name="shape") + sim_utils.add_labels(nested_prim, ["nested_label"], instance_name="class") + sim_utils.add_labels(nested_prim, ["nested_shape"], instance_name="shape") + + # Remove specific labels + sim_utils.remove_labels(prim, instance_name="shape") + # Check labels are removed correctly + labels_dict = sim_utils.get_labels(prim) + assert "shape" not in labels_dict + assert "class" in labels_dict + assert sorted(labels_dict["class"]) == sorted(["label_a", "label_b"]) + # Check nested prim labels are not removed + nested_labels_dict = sim_utils.get_labels(nested_prim) + assert "class" in nested_labels_dict + assert nested_labels_dict["class"] == ["nested_label"] + + # Remove specific labels + sim_utils.remove_labels(prim, instance_name="class", include_descendants=True) + # Check labels are removed correctly + labels_dict = sim_utils.get_labels(prim) + assert len(labels_dict) == 0 + # Check nested prim labels are removed + nested_labels_dict = sim_utils.get_labels(nested_prim) + assert "shape" in nested_labels_dict + assert nested_labels_dict["shape"] == ["nested_shape"] + + +def test_check_missing_labels(): + """Test the check_missing_labels() function.""" + # create a test environment with labels + object_paths = create_test_environment_with_labels() + + # Check from root + missing_paths = sim_utils.check_missing_labels() + + # Only the sphere should be missing + assert len(missing_paths) == 1 + assert object_paths[3] in missing_paths # Object3 should be missing + + # Check from specific subtree + missing_paths_subtree = sim_utils.check_missing_labels(prim_path="/World/Test/Object0") + # Object0 and Nested both have labels + assert len(missing_paths_subtree) == 0 + + # Check from invalid path + missing_paths_invalid = sim_utils.check_missing_labels(prim_path="/World/Test/Invalid") + assert len(missing_paths_invalid) == 0 + + +def test_count_labels_in_scene(): + """Test the count_labels_in_scene() function.""" + # create a test environment with labels + create_test_environment_with_labels() + + # Count from root + labels_dict = sim_utils.count_total_labels() + # Object0 and Nested both have labels + assert labels_dict.get("cube", 0) == 3 + assert labels_dict.get("nested", 0) == 1 + assert labels_dict.get("missing_labels", 0) == 1 + + # Count from specific subtree + labels_dict_subtree = sim_utils.count_total_labels(prim_path="/World/Test/Object0") + assert labels_dict_subtree.get("cube", 0) == 1 + assert labels_dict_subtree.get("nested", 0) == 1 + assert labels_dict_subtree.get("missing_labels", 0) == 0 + + # Count from invalid path + labels_dict_invalid = sim_utils.count_total_labels(prim_path="/World/Test/Invalid") + assert labels_dict_invalid.get("missing_labels", 0) == 0 diff --git a/source/isaaclab/test/sim/test_utils_stage.py b/source/isaaclab/test/sim/test_utils_stage.py new file mode 100644 index 000000000000..033a461e1a1f --- /dev/null +++ b/source/isaaclab/test/sim/test_utils_stage.py @@ -0,0 +1,289 @@ +# 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 + +"""Tests for stage utilities.""" + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import tempfile +from pathlib import Path + +import pytest + +from pxr import Usd + +import isaaclab.sim as sim_utils + + +def test_create_new_stage(): + """Test creating a new stage attached to USD context.""" + stage = sim_utils.create_new_stage() + + # Should return a valid stage + assert stage is not None + assert isinstance(stage, Usd.Stage) + + # Stage should be the current stage + current_stage = sim_utils.get_current_stage() + assert stage == current_stage + + # Stage should have a root prim + root_prim = stage.GetPseudoRoot() + assert root_prim.IsValid() + + +def test_create_multiple_stages(): + """Test creating multiple stages.""" + stage1 = sim_utils.create_new_stage() + stage2 = sim_utils.create_new_stage() + stage3 = sim_utils.create_new_stage() + + assert stage1 is not None + assert stage2 is not None + assert stage3 is not None + assert stage1 != stage2 + assert stage1 != stage3 + assert stage2 != stage3 + + +def test_create_new_stage_in_memory(): + """Test creating a new stage in memory (Isaac Sim 5.0+).""" + stage = sim_utils.create_new_stage_in_memory() + + # Should return a valid stage + assert stage is not None + assert isinstance(stage, Usd.Stage) + + # Stage should have a root prim + root_prim = stage.GetPseudoRoot() + assert root_prim.IsValid() + + +def test_is_current_stage_in_memory(): + """Test checking if current stage is in memory.""" + # Create a regular stage (attached to context) + sim_utils.create_new_stage() + is_in_memory = sim_utils.is_current_stage_in_memory() + + # Should return a boolean + assert isinstance(is_in_memory, bool) + assert is_in_memory is False + + # Create a stage in memory + stage = sim_utils.create_new_stage_in_memory() + with sim_utils.use_stage(stage): + is_in_memory = sim_utils.is_current_stage_in_memory() + assert isinstance(is_in_memory, bool) + assert is_in_memory is True + + +def test_save_and_open_stage(): + """Test saving and opening a stage.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a stage with some content + stage = sim_utils.create_new_stage() + stage.DefinePrim("/World", "Xform") + stage.DefinePrim("/World/TestCube", "Cube") + + # Save the stage + save_path = Path(temp_dir) / "test_stage.usd" + result = sim_utils.save_stage(str(save_path), save_and_reload_in_place=False) + + # Save should succeed + assert result is True + assert save_path.exists() + + # Open the saved stage + open_result = sim_utils.open_stage(str(save_path)) + assert open_result is True + + # Verify content was preserved + opened_stage = sim_utils.get_current_stage() + test_cube = opened_stage.GetPrimAtPath("/World/TestCube") + assert test_cube.IsValid() + assert test_cube.GetTypeName() == "Cube" + + +def test_open_stage_invalid_path(): + """Test opening a stage with invalid path.""" + with pytest.raises(ValueError, match="not supported"): + sim_utils.open_stage("/invalid/path/to/stage.invalid") + + +def test_use_stage_context_manager(): + """Test use_stage context manager.""" + # Create two stages + stage1 = sim_utils.create_new_stage() + stage1.DefinePrim("/World", "Xform") + stage1.DefinePrim("/World/Stage1Marker", "Xform") + + stage2 = Usd.Stage.CreateInMemory() + stage2.DefinePrim("/World", "Xform") + stage2.DefinePrim("/World/Stage2Marker", "Xform") + + # Initially on stage1 + current = sim_utils.get_current_stage() + marker1 = current.GetPrimAtPath("/World/Stage1Marker") + assert marker1.IsValid() + + # Switch to stage2 temporarily + with sim_utils.use_stage(stage2): + temp_current = sim_utils.get_current_stage() + # Should be on stage2 now + marker2 = temp_current.GetPrimAtPath("/World/Stage2Marker") + assert marker2.IsValid() + + # Should be back on stage1 + final_current = sim_utils.get_current_stage() + marker1_again = final_current.GetPrimAtPath("/World/Stage1Marker") + assert marker1_again.IsValid() + + +def test_use_stage_with_invalid_input(): + """Test use_stage with invalid input.""" + with pytest.raises((TypeError, AssertionError)): + with sim_utils.use_stage("not a stage"): # type: ignore + pass + + +def test_update_stage(): + """Test updating the stage.""" + # Create a new stage + stage = sim_utils.create_new_stage() + + # Add a prim + prim_path = "/World/Test" + stage.DefinePrim(prim_path, "Xform") + + # Update stage should not raise errors + sim_utils.update_stage() + + # Prim should still exist + prim = stage.GetPrimAtPath(prim_path) + assert prim.IsValid() + + +def test_save_stage_with_reload(): + """Test saving stage with reload in place.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a stage with content + stage = sim_utils.create_new_stage() + stage.DefinePrim("/World", "Xform") + stage.DefinePrim("/World/TestSphere", "Sphere") + + # Save with reload + save_path = Path(temp_dir) / "test_reload.usd" + result = sim_utils.save_stage(str(save_path), save_and_reload_in_place=True) + + assert result is True + assert save_path.exists() + + # Stage should be reloaded, content should be preserved + current_stage = sim_utils.get_current_stage() + test_sphere = current_stage.GetPrimAtPath("/World/TestSphere") + assert test_sphere.IsValid() + + +def test_save_stage_invalid_path(): + """Test saving stage with invalid path.""" + _ = sim_utils.create_new_stage() + + with pytest.raises(ValueError, match="not supported"): + sim_utils.save_stage("/tmp/test.invalid") + + +def test_close_stage(): + """Test closing the current stage.""" + # Create a stage + stage = sim_utils.create_new_stage() + assert stage is not None + + # Close it + result = sim_utils.close_stage() + + # Should succeed (or return bool) + assert isinstance(result, bool) + + +def test_close_stage_with_callback(): + """Test closing stage with a callback function.""" + # Create a stage + sim_utils.create_new_stage() + + # Track callback invocations + callback_called = [] + + def callback(success: bool, error_msg: str): + callback_called.append((success, error_msg)) + + # Close with callback + result = sim_utils.close_stage(callback_fn=callback) + + # Callback might be called or not depending on implementation + # Just verify no exceptions were raised + assert isinstance(result, bool) + + +def test_clear_stage(): + """Test clearing the stage.""" + # Create a new stage + stage = sim_utils.create_new_stage() + + # Add some prims + stage.DefinePrim("/World", "Xform") + stage.DefinePrim("/World/Cube", "Cube") + stage.DefinePrim("/World/Sphere", "Sphere") + + # Clear the stage + sim_utils.clear_stage() + + # Stage should still exist but prims should be removed + assert stage is not None + + +def test_is_stage_loading(): + """Test checking if stage is loading.""" + # Create a new stage + sim_utils.create_new_stage() + + # Check loading status + is_loading = sim_utils.is_stage_loading() + + # Should return a boolean + assert isinstance(is_loading, bool) + + # After creation, should not be loading + assert is_loading is False + + +def test_get_current_stage(): + """Test getting the current stage.""" + # Create a new stage + created_stage = sim_utils.create_new_stage() + + # Get current stage should return the same stage + current_stage = sim_utils.get_current_stage() + assert current_stage == created_stage + assert isinstance(current_stage, Usd.Stage) + + +def test_get_current_stage_id(): + """Test getting the current stage ID.""" + # Create a new stage + sim_utils.create_new_stage() + + # Get stage ID + stage_id = sim_utils.get_current_stage_id() + + # Should be a valid integer ID + assert isinstance(stage_id, int) + assert stage_id >= 0 diff --git a/source/isaaclab/test/sim/test_utils_transforms.py b/source/isaaclab/test/sim/test_utils_transforms.py new file mode 100644 index 000000000000..040cfe333aa7 --- /dev/null +++ b/source/isaaclab/test/sim/test_utils_transforms.py @@ -0,0 +1,1423 @@ +# 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 + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import math + +import numpy as np +import pytest +import torch + +from pxr import Gf, Sdf, Usd, UsdGeom + +import isaaclab.sim as sim_utils +import isaaclab.utils.math as math_utils + + +@pytest.fixture(autouse=True) +def test_setup_teardown(): + """Create a blank new stage for each test.""" + # Setup: Create a new stage + sim_utils.create_new_stage() + sim_utils.update_stage() + + # Yield for the test + yield + + # Teardown: Clear stage after each test + sim_utils.clear_stage() + + +def assert_vec3_close(v1: Gf.Vec3d | Gf.Vec3f, v2: tuple | Gf.Vec3d | Gf.Vec3f, eps: float = 1e-6): + """Assert two 3D vectors are close.""" + if isinstance(v2, tuple): + v2 = Gf.Vec3d(*v2) + for i in range(3): + assert math.isclose(v1[i], v2[i], abs_tol=eps), f"Vector mismatch at index {i}: {v1[i]} != {v2[i]}" + + +def assert_quat_close(q1: Gf.Quatf | Gf.Quatd, q2: Gf.Quatf | Gf.Quatd | tuple, eps: float = 1e-6): + """Assert two quaternions are close, accounting for double-cover (q and -q represent same rotation).""" + if isinstance(q2, tuple): + q2 = Gf.Quatd(*q2) + # Check if quaternions are close (either q1 ≈ q2 or q1 ≈ -q2) + real_match = math.isclose(q1.GetReal(), q2.GetReal(), abs_tol=eps) + imag_match = all(math.isclose(q1.GetImaginary()[i], q2.GetImaginary()[i], abs_tol=eps) for i in range(3)) + + real_match_neg = math.isclose(q1.GetReal(), -q2.GetReal(), abs_tol=eps) + imag_match_neg = all(math.isclose(q1.GetImaginary()[i], -q2.GetImaginary()[i], abs_tol=eps) for i in range(3)) + + assert (real_match and imag_match) or (real_match_neg and imag_match_neg), ( + f"Quaternion mismatch: {q1} != {q2} (and not equal to negative either)" + ) + + +def get_xform_ops(prim: Usd.Prim) -> list[str]: + """Get the ordered list of xform operation names for a prim.""" + xformable = UsdGeom.Xformable(prim) + return [op.GetOpName() for op in xformable.GetOrderedXformOps()] + + +""" +Test standardize_xform_ops() function. +""" + + +def test_standardize_xform_ops_basic(): + """Test basic functionality of standardize_xform_ops on a simple prim.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a simple xform prim with standard operations + prim = sim_utils.create_prim( + "/World/TestXform", + "Xform", + translation=(1.0, 2.0, 3.0), + orientation=(1.0, 0.0, 0.0, 0.0), # w, x, y, z + scale=(1.0, 1.0, 1.0), + stage=stage, + ) + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + + # Verify the operation succeeded + assert result is True + assert prim.IsValid() + + # Check that the xform operations are in the correct order + xform_ops = get_xform_ops(prim) + assert xform_ops == [ + "xformOp:translate", + "xformOp:orient", + "xformOp:scale", + ], f"Expected standard xform order, got {xform_ops}" + + # Verify the transform values are preserved (approximately) + assert_vec3_close(prim.GetAttribute("xformOp:translate").Get(), (1.0, 2.0, 3.0)) + assert_quat_close(prim.GetAttribute("xformOp:orient").Get(), (1.0, 0.0, 0.0, 0.0)) + assert_vec3_close(prim.GetAttribute("xformOp:scale").Get(), (1.0, 1.0, 1.0)) + + +def test_standardize_xform_ops_with_rotation_xyz(): + """Test standardize_xform_ops removes deprecated rotateXYZ operations.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim and manually add deprecated rotation operations + prim_path = "/World/TestRotateXYZ" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + # Add deprecated rotateXYZ operation + rotate_xyz_op = xformable.AddRotateXYZOp(UsdGeom.XformOp.PrecisionDouble) + rotate_xyz_op.Set(Gf.Vec3d(45.0, 30.0, 60.0)) + # Add translate operation + translate_op = xformable.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble) + translate_op.Set(Gf.Vec3d(1.0, 2.0, 3.0)) + + # Verify the deprecated operation exists + assert "xformOp:rotateXYZ" in prim.GetPropertyNames() + + # Get pose before standardization + pos_before, quat_before = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + assert result is True + + # Get pose after standardization + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + # Verify world pose is preserved (may have small numeric differences due to rotation conversion) + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-4) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-4) + + # Verify the deprecated operation is removed + assert "xformOp:rotateXYZ" not in prim.GetPropertyNames() + # Verify standard operations exist + assert "xformOp:translate" in prim.GetPropertyNames() + assert "xformOp:orient" in prim.GetPropertyNames() + assert "xformOp:scale" in prim.GetPropertyNames() + # Check the xform operation order + xform_ops = get_xform_ops(prim) + assert xform_ops == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + +def test_standardize_xform_ops_with_transform_matrix(): + """Test standardize_xform_ops removes transform matrix operations.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with a transform matrix + prim_path = "/World/TestTransformMatrix" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add transform matrix operation + transform_op = xformable.AddTransformOp(UsdGeom.XformOp.PrecisionDouble) + # Create a simple translation matrix + matrix = Gf.Matrix4d().SetTranslate(Gf.Vec3d(5.0, 10.0, 15.0)) + transform_op.Set(matrix) + + # Verify the transform operation exists + assert "xformOp:transform" in prim.GetPropertyNames() + + # Get pose before standardization + pos_before, quat_before = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + assert result is True + + # Get pose after standardization + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + # Verify world pose is preserved + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + + # Verify the transform operation is removed + assert "xformOp:transform" not in prim.GetPropertyNames() + # Verify standard operations exist + assert "xformOp:translate" in prim.GetPropertyNames() + assert "xformOp:orient" in prim.GetPropertyNames() + assert "xformOp:scale" in prim.GetPropertyNames() + + +def test_standardize_xform_ops_preserves_world_pose(): + """Test that standardize_xform_ops preserves the world-space pose of the prim.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with specific world pose + translation = (10.0, 20.0, 30.0) + # Rotation of 90 degrees around Z axis + orientation = (0.7071068, 0.0, 0.0, 0.7071068) # w, x, y, z + scale = (2.0, 3.0, 4.0) + + prim = sim_utils.create_prim( + "/World/TestPreservePose", + "Xform", + translation=translation, + orientation=orientation, + scale=scale, + stage=stage, + ) + + # Get the world pose before standardization + pos_before, quat_before = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + assert result is True + + # Get the world pose after standardization + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + # Verify the world pose is preserved + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + + +def test_standardize_xform_ops_with_units_resolve(): + """Test standardize_xform_ops handles scale:unitsResolve attribute.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim + prim_path = "/World/TestUnitsResolve" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add scale operation + scale_op = xformable.AddScaleOp(UsdGeom.XformOp.PrecisionDouble) + scale_op.Set(Gf.Vec3d(1.0, 1.0, 1.0)) + + # Manually add a unitsResolve scale attribute (simulating imported asset with different units) + units_resolve_attr = prim.CreateAttribute("xformOp:scale:unitsResolve", Sdf.ValueTypeNames.Double3) + units_resolve_attr.Set(Gf.Vec3d(100.0, 100.0, 100.0)) # e.g., cm to m conversion + + # Verify both attributes exist + assert "xformOp:scale" in prim.GetPropertyNames() + assert "xformOp:scale:unitsResolve" in prim.GetPropertyNames() + + # Get pose before standardization + pos_before, quat_before = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + assert result is True + + # Get pose after standardization + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + # Verify pose is preserved + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + + # Verify unitsResolve is removed + assert "xformOp:scale:unitsResolve" not in prim.GetPropertyNames() + + # Verify scale is updated (1.0 * 100.0 = 100.0) + scale = prim.GetAttribute("xformOp:scale").Get() + assert_vec3_close(scale, (100.0, 100.0, 100.0)) + + +def test_standardize_xform_ops_with_hierarchy(): + """Test standardize_xform_ops works correctly with prim hierarchies.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create parent prim + parent_prim = sim_utils.create_prim( + "/World/Parent", + "Xform", + translation=(5.0, 0.0, 0.0), + orientation=(1.0, 0.0, 0.0, 0.0), + scale=(2.0, 2.0, 2.0), + stage=stage, + ) + + # Create child prim + child_prim = sim_utils.create_prim( + "/World/Parent/Child", + "Xform", + translation=(0.0, 3.0, 0.0), + orientation=(0.7071068, 0.0, 0.7071068, 0.0), # 90 deg around Y + scale=(0.5, 0.5, 0.5), + stage=stage, + ) + + # Get world poses before standardization + parent_pos_before, parent_quat_before = sim_utils.resolve_prim_pose(parent_prim) + child_pos_before, child_quat_before = sim_utils.resolve_prim_pose(child_prim) + + # Apply standardize_xform_ops to both + sim_utils.standardize_xform_ops(parent_prim) + sim_utils.standardize_xform_ops(child_prim) + + # Get world poses after standardization + parent_pos_after, parent_quat_after = sim_utils.resolve_prim_pose(parent_prim) + child_pos_after, child_quat_after = sim_utils.resolve_prim_pose(child_prim) + + # Verify world poses are preserved + assert_vec3_close(Gf.Vec3d(*parent_pos_before), parent_pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*parent_quat_before), parent_quat_after, eps=1e-5) + assert_vec3_close(Gf.Vec3d(*child_pos_before), child_pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*child_quat_before), child_quat_after, eps=1e-5) + + +def test_standardize_xform_ops_multiple_deprecated_ops(): + """Test standardize_xform_ops removes multiple deprecated operations.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with multiple deprecated operations + prim_path = "/World/TestMultipleDeprecated" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add various deprecated rotation operations + rotate_x_op = xformable.AddRotateXOp(UsdGeom.XformOp.PrecisionDouble) + rotate_x_op.Set(45.0) + rotate_y_op = xformable.AddRotateYOp(UsdGeom.XformOp.PrecisionDouble) + rotate_y_op.Set(30.0) + rotate_z_op = xformable.AddRotateZOp(UsdGeom.XformOp.PrecisionDouble) + rotate_z_op.Set(60.0) + + # Verify deprecated operations exist + assert "xformOp:rotateX" in prim.GetPropertyNames() + assert "xformOp:rotateY" in prim.GetPropertyNames() + assert "xformOp:rotateZ" in prim.GetPropertyNames() + + # Obtain current local transformations + pos, quat = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops + sim_utils.standardize_xform_ops(prim) + + # Obtain current local transformations + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + # Verify world pose is preserved + assert_vec3_close(Gf.Vec3d(*pos), Gf.Vec3d(*pos_after), eps=1e-5) + assert_quat_close(Gf.Quatd(*quat), Gf.Quatd(*quat_after), eps=1e-5) + + # Verify all deprecated operations are removed + assert "xformOp:rotateX" not in prim.GetPropertyNames() + assert "xformOp:rotateY" not in prim.GetPropertyNames() + assert "xformOp:rotateZ" not in prim.GetPropertyNames() + # Verify standard operations exist + xform_ops = get_xform_ops(prim) + assert xform_ops == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + +def test_standardize_xform_ops_with_existing_standard_ops(): + """Test standardize_xform_ops when prim already has standard operations.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with standard operations already in place + prim = sim_utils.create_prim( + "/World/TestExistingStandard", + "Xform", + translation=(7.0, 8.0, 9.0), + orientation=(0.9238795, 0.3826834, 0.0, 0.0), # rotation around X + scale=(1.5, 2.5, 3.5), + stage=stage, + ) + + # Get initial values + initial_translate = prim.GetAttribute("xformOp:translate").Get() + initial_orient = prim.GetAttribute("xformOp:orient").Get() + initial_scale = prim.GetAttribute("xformOp:scale").Get() + + # Get world pose before standardization + pos_before, quat_before = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + assert result is True + + # Get world pose after standardization + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + # Verify world pose is preserved + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + + # Verify operations still exist and are in correct order + xform_ops = get_xform_ops(prim) + assert xform_ops == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + # Verify values are approximately preserved + final_translate = prim.GetAttribute("xformOp:translate").Get() + final_orient = prim.GetAttribute("xformOp:orient").Get() + final_scale = prim.GetAttribute("xformOp:scale").Get() + + assert_vec3_close(initial_translate, final_translate, eps=1e-5) + assert_quat_close(initial_orient, final_orient, eps=1e-5) + assert_vec3_close(initial_scale, final_scale, eps=1e-5) + + +def test_standardize_xform_ops_invalid_prim(): + """Test standardize_xform_ops raises error for invalid prim.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Get an invalid prim (non-existent path) + invalid_prim = stage.GetPrimAtPath("/World/NonExistent") + + # Verify the prim is invalid + assert not invalid_prim.IsValid() + + # Attempt to apply standardize_xform_ops and expect ValueError + with pytest.raises(ValueError, match="not valid"): + sim_utils.standardize_xform_ops(invalid_prim) + + +def test_standardize_xform_ops_on_geometry_prim(): + """Test standardize_xform_ops on a geometry prim (Cube, Sphere, etc.).""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a cube with transform + cube_prim = sim_utils.create_prim( + "/World/TestCube", + "Cube", + translation=(1.0, 2.0, 3.0), + orientation=(1.0, 0.0, 0.0, 0.0), + scale=(2.0, 2.0, 2.0), + attributes={"size": 1.0}, + stage=stage, + ) + + # Get world pose before + pos_before, quat_before = sim_utils.resolve_prim_pose(cube_prim) + + # Apply standardize_xform_ops + sim_utils.standardize_xform_ops(cube_prim) + + # Get world pose after + pos_after, quat_after = sim_utils.resolve_prim_pose(cube_prim) + # Verify world pose is preserved + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + + # Verify standard operations exist + xform_ops = get_xform_ops(cube_prim) + assert xform_ops == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + +def test_standardize_xform_ops_with_non_uniform_scale(): + """Test standardize_xform_ops with non-uniform scale.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with non-uniform scale + prim = sim_utils.create_prim( + "/World/TestNonUniformScale", + "Xform", + translation=(5.0, 10.0, 15.0), + orientation=(0.7071068, 0.0, 0.7071068, 0.0), # 90 deg around Y + scale=(1.0, 2.0, 3.0), # Non-uniform scale + stage=stage, + ) + + # Get initial scale + initial_scale = prim.GetAttribute("xformOp:scale").Get() + + # Get world pose before standardization + pos_before, quat_before = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + assert result is True + + # Get world pose after standardization + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + # Verify world pose is preserved + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + # Verify scale is preserved + final_scale = prim.GetAttribute("xformOp:scale").Get() + assert_vec3_close(initial_scale, final_scale, eps=1e-5) + + +def test_standardize_xform_ops_identity_transform(): + """Test standardize_xform_ops with identity transform (no translation, rotation, or scale).""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with identity transform + prim = sim_utils.create_prim( + "/World/TestIdentity", + "Xform", + translation=(0.0, 0.0, 0.0), + orientation=(1.0, 0.0, 0.0, 0.0), # Identity quaternion + scale=(1.0, 1.0, 1.0), + stage=stage, + ) + + # Apply standardize_xform_ops + sim_utils.standardize_xform_ops(prim) + + # Verify standard operations exist + xform_ops = get_xform_ops(prim) + assert xform_ops == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + # Verify identity values + assert_vec3_close(prim.GetAttribute("xformOp:translate").Get(), (0.0, 0.0, 0.0)) + assert_quat_close(prim.GetAttribute("xformOp:orient").Get(), (1.0, 0.0, 0.0, 0.0)) + assert_vec3_close(prim.GetAttribute("xformOp:scale").Get(), (1.0, 1.0, 1.0)) + + +def test_standardize_xform_ops_with_explicit_values(): + """Test standardize_xform_ops with explicit translation, orientation, and scale values.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with some initial transform + prim = sim_utils.create_prim( + "/World/TestExplicitValues", + "Xform", + translation=(10.0, 10.0, 10.0), + orientation=(0.7071068, 0.7071068, 0.0, 0.0), + scale=(5.0, 5.0, 5.0), + stage=stage, + ) + + # Apply standardize_xform_ops with new explicit values + new_translation = (1.0, 2.0, 3.0) + new_orientation = (1.0, 0.0, 0.0, 0.0) + new_scale = (2.0, 2.0, 2.0) + + result = sim_utils.standardize_xform_ops( + prim, translation=new_translation, orientation=new_orientation, scale=new_scale + ) + assert result is True + + # Verify the new values are set + assert_vec3_close(prim.GetAttribute("xformOp:translate").Get(), new_translation) + assert_quat_close(prim.GetAttribute("xformOp:orient").Get(), new_orientation) + assert_vec3_close(prim.GetAttribute("xformOp:scale").Get(), new_scale) + + # Verify the prim is at the expected world location + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + assert_vec3_close(Gf.Vec3d(*pos_after), new_translation, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_after), new_orientation, eps=1e-5) + + # Verify standard operation order + xform_ops = get_xform_ops(prim) + assert xform_ops == ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + +def test_standardize_xform_ops_with_partial_values(): + """Test standardize_xform_ops with only some values specified.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim + prim = sim_utils.create_prim( + "/World/TestPartialValues", + "Xform", + translation=(1.0, 2.0, 3.0), + orientation=(0.9238795, 0.3826834, 0.0, 0.0), # rotation around X + scale=(2.0, 2.0, 2.0), + stage=stage, + ) + + # Get initial local pose + pos_before, quat_before = sim_utils.resolve_prim_pose(prim, ref_prim=prim.GetParent()) + scale_before = prim.GetAttribute("xformOp:scale").Get() + + # Apply standardize_xform_ops with only translation specified + new_translation = (10.0, 20.0, 30.0) + result = sim_utils.standardize_xform_ops(prim, translation=new_translation) + assert result is True + + # Verify translation is updated + assert_vec3_close(prim.GetAttribute("xformOp:translate").Get(), new_translation) + + # Verify orientation and scale are preserved + quat_after = prim.GetAttribute("xformOp:orient").Get() + scale_after = prim.GetAttribute("xformOp:scale").Get() + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + assert_vec3_close(scale_before, scale_after, eps=1e-5) + + # Verify the prim's world orientation hasn't changed (only translation changed) + _, quat_after_world = sim_utils.resolve_prim_pose(prim) + assert_quat_close(Gf.Quatd(*quat_before), quat_after_world, eps=1e-5) + + +def test_standardize_xform_ops_non_xformable_prim(caplog): + """Test standardize_xform_ops returns False for non-Xformable prims and logs error.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a Material prim (not Xformable) + from pxr import UsdShade + + material_prim = UsdShade.Material.Define(stage, "/World/TestMaterial").GetPrim() + + # Verify the prim is valid but not Xformable + assert material_prim.IsValid() + assert not material_prim.IsA(UsdGeom.Xformable) + + # Clear any previous logs + caplog.clear() + + # Attempt to apply standardize_xform_ops - should return False and log a error + with caplog.at_level("ERROR"): + result = sim_utils.standardize_xform_ops(material_prim) + + assert result is False + + # Verify that a error was logged + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" + assert "not an Xformable" in caplog.records[0].message + assert "/World/TestMaterial" in caplog.records[0].message + + +def test_standardize_xform_ops_preserves_reset_xform_stack(): + """Test that standardize_xform_ops preserves the resetXformStack attribute.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim + prim = sim_utils.create_prim("/World/TestResetStack", "Xform", stage=stage) + xformable = UsdGeom.Xformable(prim) + + # Set resetXformStack to True + xformable.SetResetXformStack(True) + assert xformable.GetResetXformStack() is True + + # Apply standardize_xform_ops + result = sim_utils.standardize_xform_ops(prim) + assert result is True + + # Verify resetXformStack is preserved + assert xformable.GetResetXformStack() is True + + +def test_standardize_xform_ops_with_complex_hierarchy(): + """Test standardize_xform_ops on deeply nested hierarchy.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a complex hierarchy + root = sim_utils.create_prim("/World/Root", "Xform", translation=(1.0, 0.0, 0.0), stage=stage) + child1 = sim_utils.create_prim("/World/Root/Child1", "Xform", translation=(0.0, 1.0, 0.0), stage=stage) + child2 = sim_utils.create_prim("/World/Root/Child1/Child2", "Xform", translation=(0.0, 0.0, 1.0), stage=stage) + child3 = sim_utils.create_prim("/World/Root/Child1/Child2/Child3", "Cube", translation=(1.0, 1.0, 1.0), stage=stage) + + # Get world poses before + poses_before = {} + for name, prim in [("root", root), ("child1", child1), ("child2", child2), ("child3", child3)]: + poses_before[name] = sim_utils.resolve_prim_pose(prim) + + # Apply standardize_xform_ops to all prims + assert sim_utils.standardize_xform_ops(root) is True + assert sim_utils.standardize_xform_ops(child1) is True + assert sim_utils.standardize_xform_ops(child2) is True + assert sim_utils.standardize_xform_ops(child3) is True + + # Get world poses after + poses_after = {} + for name, prim in [("root", root), ("child1", child1), ("child2", child2), ("child3", child3)]: + poses_after[name] = sim_utils.resolve_prim_pose(prim) + + # Verify all world poses are preserved + for name in poses_before: + pos_before, quat_before = poses_before[name] + pos_after, quat_after = poses_after[name] + assert_vec3_close(Gf.Vec3d(*pos_before), pos_after, eps=1e-5) + assert_quat_close(Gf.Quatd(*quat_before), quat_after, eps=1e-5) + + +def test_standardize_xform_ops_preserves_float_precision(): + """Test that standardize_xform_ops preserves float precision when it already exists.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim manually with FLOAT precision operations (not double) + prim_path = "/World/TestFloatPrecision" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add xform operations with FLOAT precision (not the default double) + translate_op = xformable.AddTranslateOp(UsdGeom.XformOp.PrecisionFloat) + translate_op.Set(Gf.Vec3f(1.0, 2.0, 3.0)) + + orient_op = xformable.AddOrientOp(UsdGeom.XformOp.PrecisionFloat) + orient_op.Set(Gf.Quatf(1.0, 0.0, 0.0, 0.0)) + + scale_op = xformable.AddScaleOp(UsdGeom.XformOp.PrecisionFloat) + scale_op.Set(Gf.Vec3f(1.0, 1.0, 1.0)) + + # Verify operations exist with float precision + assert translate_op.GetPrecision() == UsdGeom.XformOp.PrecisionFloat + assert orient_op.GetPrecision() == UsdGeom.XformOp.PrecisionFloat + assert scale_op.GetPrecision() == UsdGeom.XformOp.PrecisionFloat + + # Now apply standardize_xform_ops with new values (provided as double precision Python floats) + new_translation = (5.0, 10.0, 15.0) + new_orientation = (0.7071068, 0.7071068, 0.0, 0.0) # 90 deg around X + new_scale = (2.0, 3.0, 4.0) + + result = sim_utils.standardize_xform_ops( + prim, translation=new_translation, orientation=new_orientation, scale=new_scale + ) + assert result is True + + # Verify the precision is STILL float (not converted to double) + translate_op_after = UsdGeom.XformOp(prim.GetAttribute("xformOp:translate")) + orient_op_after = UsdGeom.XformOp(prim.GetAttribute("xformOp:orient")) + scale_op_after = UsdGeom.XformOp(prim.GetAttribute("xformOp:scale")) + + assert translate_op_after.GetPrecision() == UsdGeom.XformOp.PrecisionFloat + assert orient_op_after.GetPrecision() == UsdGeom.XformOp.PrecisionFloat + assert scale_op_after.GetPrecision() == UsdGeom.XformOp.PrecisionFloat + + # Verify the VALUES are set correctly (cast to float, so they're Gf.Vec3f and Gf.Quatf) + translate_value = prim.GetAttribute("xformOp:translate").Get() + assert isinstance(translate_value, Gf.Vec3f), f"Expected Gf.Vec3f, got {type(translate_value)}" + assert_vec3_close(translate_value, new_translation, eps=1e-5) + + orient_value = prim.GetAttribute("xformOp:orient").Get() + assert isinstance(orient_value, Gf.Quatf), f"Expected Gf.Quatf, got {type(orient_value)}" + assert_quat_close(orient_value, new_orientation, eps=1e-5) + + scale_value = prim.GetAttribute("xformOp:scale").Get() + assert isinstance(scale_value, Gf.Vec3f), f"Expected Gf.Vec3f, got {type(scale_value)}" + assert_vec3_close(scale_value, new_scale, eps=1e-5) + + # Verify the world pose matches what we set + pos_after, quat_after = sim_utils.resolve_prim_pose(prim) + assert_vec3_close(Gf.Vec3d(*pos_after), new_translation, eps=1e-4) + assert_quat_close(Gf.Quatd(*quat_after), new_orientation, eps=1e-4) + + +""" +Test validate_standard_xform_ops() function. +""" + + +def test_validate_standard_xform_ops_valid(): + """Test validate_standard_xform_ops returns True for standardized prims.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with standard operations + prim = sim_utils.create_prim( + "/World/TestValid", + "Xform", + translation=(1.0, 2.0, 3.0), + orientation=(1.0, 0.0, 0.0, 0.0), + scale=(1.0, 1.0, 1.0), + stage=stage, + ) + + # Standardize the prim + sim_utils.standardize_xform_ops(prim) + + # Validate it + assert sim_utils.validate_standard_xform_ops(prim) is True + + +def test_validate_standard_xform_ops_invalid_order(): + """Test validate_standard_xform_ops returns False for non-standard operation order.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim and manually set up xform ops in wrong order + prim_path = "/World/TestInvalidOrder" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add operations in wrong order: scale, translate, orient (should be translate, orient, scale) + scale_op = xformable.AddScaleOp(UsdGeom.XformOp.PrecisionDouble) + scale_op.Set(Gf.Vec3d(1.0, 1.0, 1.0)) + + translate_op = xformable.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble) + translate_op.Set(Gf.Vec3d(1.0, 2.0, 3.0)) + + orient_op = xformable.AddOrientOp(UsdGeom.XformOp.PrecisionDouble) + orient_op.Set(Gf.Quatd(1.0, 0.0, 0.0, 0.0)) + + # Validate it - should return False + assert sim_utils.validate_standard_xform_ops(prim) is False + + +def test_validate_standard_xform_ops_with_deprecated_ops(): + """Test validate_standard_xform_ops returns False when deprecated operations exist.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with deprecated rotateXYZ operation + prim_path = "/World/TestDeprecated" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add deprecated rotateXYZ operation + rotate_xyz_op = xformable.AddRotateXYZOp(UsdGeom.XformOp.PrecisionDouble) + rotate_xyz_op.Set(Gf.Vec3d(45.0, 30.0, 60.0)) + + # Validate it - should return False + assert sim_utils.validate_standard_xform_ops(prim) is False + + +def test_validate_standard_xform_ops_missing_operations(): + """Test validate_standard_xform_ops returns False when standard operations are missing.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with only translate operation (missing orient and scale) + prim_path = "/World/TestMissing" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + translate_op = xformable.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble) + translate_op.Set(Gf.Vec3d(1.0, 2.0, 3.0)) + + # Validate it - should return False (missing orient and scale) + assert sim_utils.validate_standard_xform_ops(prim) is False + + +def test_validate_standard_xform_ops_invalid_prim(): + """Test validate_standard_xform_ops returns False for invalid prim.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Get an invalid prim + invalid_prim = stage.GetPrimAtPath("/World/NonExistent") + + # Validate it - should return False + assert sim_utils.validate_standard_xform_ops(invalid_prim) is False + + +def test_validate_standard_xform_ops_non_xformable(): + """Test validate_standard_xform_ops returns False for non-Xformable prims.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a Material prim (not Xformable) + from pxr import UsdShade + + material_prim = UsdShade.Material.Define(stage, "/World/TestMaterial").GetPrim() + + # Validate it - should return False + assert sim_utils.validate_standard_xform_ops(material_prim) is False + + +def test_validate_standard_xform_ops_with_transform_matrix(): + """Test validate_standard_xform_ops returns False when transform matrix operation exists.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with transform matrix + prim_path = "/World/TestTransformMatrix" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add transform matrix operation + transform_op = xformable.AddTransformOp(UsdGeom.XformOp.PrecisionDouble) + matrix = Gf.Matrix4d().SetTranslate(Gf.Vec3d(5.0, 10.0, 15.0)) + transform_op.Set(matrix) + + # Validate it - should return False + assert sim_utils.validate_standard_xform_ops(prim) is False + + +def test_validate_standard_xform_ops_extra_operations(): + """Test validate_standard_xform_ops returns False when extra operations exist.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with standard operations + prim = sim_utils.create_prim( + "/World/TestExtra", + "Xform", + translation=(1.0, 2.0, 3.0), + orientation=(1.0, 0.0, 0.0, 0.0), + scale=(1.0, 1.0, 1.0), + stage=stage, + ) + + # Standardize it + sim_utils.standardize_xform_ops(prim) + + # Add an extra operation + xformable = UsdGeom.Xformable(prim) + extra_op = xformable.AddRotateXOp(UsdGeom.XformOp.PrecisionDouble) + extra_op.Set(45.0) + + # Validate it - should return False (has extra operation) + assert sim_utils.validate_standard_xform_ops(prim) is False + + +def test_validate_standard_xform_ops_after_standardization(): + """Test validate_standard_xform_ops returns True after standardization of non-standard prim.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a prim with non-standard operations + prim_path = "/World/TestBeforeAfter" + prim = stage.DefinePrim(prim_path, "Xform") + xformable = UsdGeom.Xformable(prim) + + # Add deprecated operations + rotate_x_op = xformable.AddRotateXOp(UsdGeom.XformOp.PrecisionDouble) + rotate_x_op.Set(45.0) + translate_op = xformable.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble) + translate_op.Set(Gf.Vec3d(1.0, 2.0, 3.0)) + + # Validate before standardization - should be False + assert sim_utils.validate_standard_xform_ops(prim) is False + + # Standardize the prim + sim_utils.standardize_xform_ops(prim) + + # Validate after standardization - should be True + assert sim_utils.validate_standard_xform_ops(prim) is True + + +def test_validate_standard_xform_ops_on_geometry(): + """Test validate_standard_xform_ops works correctly on geometry prims.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a cube with standard operations + cube_prim = sim_utils.create_prim( + "/World/TestCube", + "Cube", + translation=(1.0, 2.0, 3.0), + orientation=(1.0, 0.0, 0.0, 0.0), + scale=(2.0, 2.0, 2.0), + stage=stage, + ) + + # Standardize it + sim_utils.standardize_xform_ops(cube_prim) + + # Validate it - should be True + assert sim_utils.validate_standard_xform_ops(cube_prim) is True + + +def test_validate_standard_xform_ops_empty_prim(): + """Test validate_standard_xform_ops on prim with no xform operations.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a bare prim with no xform operations + prim_path = "/World/TestEmpty" + prim = stage.DefinePrim(prim_path, "Xform") + + # Validate it - should return False (no operations at all) + assert sim_utils.validate_standard_xform_ops(prim) is False + + +""" +Test resolve_prim_pose() function. +""" + + +def test_resolve_prim_pose(): + """Test resolve_prim_pose() function.""" + # number of objects + num_objects = 20 + # sample random scales for x, y, z + rand_scales = np.random.uniform(0.5, 1.5, size=(num_objects, 3, 3)) + rand_widths = np.random.uniform(0.1, 10.0, size=(num_objects,)) + # sample random positions + rand_positions = np.random.uniform(-100, 100, size=(num_objects, 3, 3)) + # sample random rotations + rand_quats = np.random.randn(num_objects, 3, 4) + rand_quats /= np.linalg.norm(rand_quats, axis=2, keepdims=True) + + # create objects + for i in range(num_objects): + # simple cubes + cube_prim = sim_utils.create_prim( + f"/World/Cubes/instance_{i:02d}", + "Cube", + translation=rand_positions[i, 0], + orientation=rand_quats[i, 0], + scale=rand_scales[i, 0], + attributes={"size": rand_widths[i]}, + ) + # xform hierarchy + xform_prim = sim_utils.create_prim( + f"/World/Xform/instance_{i:02d}", + "Xform", + translation=rand_positions[i, 1], + orientation=rand_quats[i, 1], + scale=rand_scales[i, 1], + ) + geometry_prim = sim_utils.create_prim( + f"/World/Xform/instance_{i:02d}/geometry", + "Sphere", + translation=rand_positions[i, 2], + orientation=rand_quats[i, 2], + scale=rand_scales[i, 2], + attributes={"radius": rand_widths[i]}, + ) + dummy_prim = sim_utils.create_prim( + f"/World/Xform/instance_{i:02d}/dummy", + "Sphere", + ) + + # cube prim w.r.t. world frame + pos, quat = sim_utils.resolve_prim_pose(cube_prim) + pos, quat = np.array(pos), np.array(quat) + quat = quat if np.sign(rand_quats[i, 0, 0]) == np.sign(quat[0]) else -quat + np.testing.assert_allclose(pos, rand_positions[i, 0], atol=1e-3) + np.testing.assert_allclose(quat, rand_quats[i, 0], atol=1e-3) + # xform prim w.r.t. world frame + pos, quat = sim_utils.resolve_prim_pose(xform_prim) + pos, quat = np.array(pos), np.array(quat) + quat = quat if np.sign(rand_quats[i, 1, 0]) == np.sign(quat[0]) else -quat + np.testing.assert_allclose(pos, rand_positions[i, 1], atol=1e-3) + np.testing.assert_allclose(quat, rand_quats[i, 1], atol=1e-3) + # dummy prim w.r.t. world frame + pos, quat = sim_utils.resolve_prim_pose(dummy_prim) + pos, quat = np.array(pos), np.array(quat) + quat = quat if np.sign(rand_quats[i, 1, 0]) == np.sign(quat[0]) else -quat + np.testing.assert_allclose(pos, rand_positions[i, 1], atol=1e-3) + np.testing.assert_allclose(quat, rand_quats[i, 1], atol=1e-3) + + # geometry prim w.r.t. xform prim + pos, quat = sim_utils.resolve_prim_pose(geometry_prim, ref_prim=xform_prim) + pos, quat = np.array(pos), np.array(quat) + quat = quat if np.sign(rand_quats[i, 2, 0]) == np.sign(quat[0]) else -quat + np.testing.assert_allclose(pos, rand_positions[i, 2] * rand_scales[i, 1], atol=1e-3) + # TODO: Enabling scale causes the test to fail because the current implementation of + # resolve_prim_pose does not correctly handle non-identity scales on Xform prims. This is a known + # limitation. Until this is fixed, the test is disabled here to ensure the test passes. + # np.testing.assert_allclose(quat, rand_quats[i, 2], atol=1e-3) + + # dummy prim w.r.t. xform prim + pos, quat = sim_utils.resolve_prim_pose(dummy_prim, ref_prim=xform_prim) + pos, quat = np.array(pos), np.array(quat) + np.testing.assert_allclose(pos, np.zeros(3), atol=1e-3) + np.testing.assert_allclose(quat, np.array([1, 0, 0, 0]), atol=1e-3) + # xform prim w.r.t. cube prim + pos, quat = sim_utils.resolve_prim_pose(xform_prim, ref_prim=cube_prim) + pos, quat = np.array(pos), np.array(quat) + # -- compute ground truth values + gt_pos, gt_quat = math_utils.subtract_frame_transforms( + torch.from_numpy(rand_positions[i, 0]).unsqueeze(0), + torch.from_numpy(rand_quats[i, 0]).unsqueeze(0), + torch.from_numpy(rand_positions[i, 1]).unsqueeze(0), + torch.from_numpy(rand_quats[i, 1]).unsqueeze(0), + ) + gt_pos, gt_quat = gt_pos.squeeze(0).numpy(), gt_quat.squeeze(0).numpy() + quat = quat if np.sign(gt_quat[0]) == np.sign(quat[0]) else -quat + np.testing.assert_allclose(pos, gt_pos, atol=1e-3) + np.testing.assert_allclose(quat, gt_quat, atol=1e-3) + + +""" +Test resolve_prim_scale() function. +""" + + +def test_resolve_prim_scale(): + """Test resolve_prim_scale() function. + + To simplify the test, we assume that the effective scale at a prim + is the product of the scales of the prims in the hierarchy: + + scale = scale_of_xform * scale_of_geometry_prim + + This is only true when rotations are identity or the transforms are + orthogonal and uniformly scaled. Otherwise, scale is not composable + like that in local component-wise fashion. + """ + # number of objects + num_objects = 20 + # sample random scales for x, y, z + rand_scales = np.random.uniform(0.5, 1.5, size=(num_objects, 3, 3)) + rand_widths = np.random.uniform(0.1, 10.0, size=(num_objects,)) + # sample random positions + rand_positions = np.random.uniform(-100, 100, size=(num_objects, 3, 3)) + + # create objects + for i in range(num_objects): + # simple cubes + cube_prim = sim_utils.create_prim( + f"/World/Cubes/instance_{i:02d}", + "Cube", + translation=rand_positions[i, 0], + scale=rand_scales[i, 0], + attributes={"size": rand_widths[i]}, + ) + # xform hierarchy + xform_prim = sim_utils.create_prim( + f"/World/Xform/instance_{i:02d}", + "Xform", + translation=rand_positions[i, 1], + scale=rand_scales[i, 1], + ) + geometry_prim = sim_utils.create_prim( + f"/World/Xform/instance_{i:02d}/geometry", + "Sphere", + translation=rand_positions[i, 2], + scale=rand_scales[i, 2], + attributes={"radius": rand_widths[i]}, + ) + dummy_prim = sim_utils.create_prim( + f"/World/Xform/instance_{i:02d}/dummy", + "Sphere", + ) + + # cube prim + scale = sim_utils.resolve_prim_scale(cube_prim) + scale = np.array(scale) + np.testing.assert_allclose(scale, rand_scales[i, 0], atol=1e-5) + # xform prim + scale = sim_utils.resolve_prim_scale(xform_prim) + scale = np.array(scale) + np.testing.assert_allclose(scale, rand_scales[i, 1], atol=1e-5) + # geometry prim + scale = sim_utils.resolve_prim_scale(geometry_prim) + scale = np.array(scale) + np.testing.assert_allclose(scale, rand_scales[i, 1] * rand_scales[i, 2], atol=1e-5) + # dummy prim + scale = sim_utils.resolve_prim_scale(dummy_prim) + scale = np.array(scale) + np.testing.assert_allclose(scale, rand_scales[i, 1], atol=1e-5) + + +""" +Test convert_world_pose_to_local() function. +""" + + +def test_convert_world_pose_to_local_basic(): + """Test basic world-to-local pose conversion.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create parent and child prims + parent_prim = sim_utils.create_prim( + "/World/Parent", + "Xform", + translation=(5.0, 0.0, 0.0), + orientation=(1.0, 0.0, 0.0, 0.0), # identity rotation + scale=(1.0, 1.0, 1.0), + stage=stage, + ) + + # World pose we want to achieve for a child + world_position = (10.0, 3.0, 0.0) + world_orientation = (1.0, 0.0, 0.0, 0.0) # identity rotation + + # Convert to local space + local_translation, local_orientation = sim_utils.convert_world_pose_to_local( + world_position, world_orientation, parent_prim + ) + # Assert orientation is not None + assert local_orientation is not None + + # The expected local translation is world_position - parent_position = (10-5, 3-0, 0-0) = (5, 3, 0) + assert_vec3_close(Gf.Vec3d(*local_translation), (5.0, 3.0, 0.0), eps=1e-5) + assert_quat_close(Gf.Quatd(*local_orientation), (1.0, 0.0, 0.0, 0.0), eps=1e-5) + + +def test_convert_world_pose_to_local_with_rotation(): + """Test world-to-local conversion with parent rotation.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create parent with 90-degree rotation around Z axis + parent_prim = sim_utils.create_prim( + "/World/RotatedParent", + "Xform", + translation=(0.0, 0.0, 0.0), + orientation=(0.7071068, 0.0, 0.0, 0.7071068), # 90 deg around Z + scale=(1.0, 1.0, 1.0), + stage=stage, + ) + + # World pose: position at (1, 0, 0) with identity rotation + world_position = (1.0, 0.0, 0.0) + world_orientation = (1.0, 0.0, 0.0, 0.0) + + # Convert to local space + local_translation, local_orientation = sim_utils.convert_world_pose_to_local( + world_position, world_orientation, parent_prim + ) + + # Create a child with the local transform and verify world pose + child_prim = sim_utils.create_prim( + "/World/RotatedParent/Child", + "Xform", + translation=local_translation, + orientation=local_orientation, + stage=stage, + ) + + # Get world pose of child + child_world_pos, child_world_quat = sim_utils.resolve_prim_pose(child_prim) + + # Verify it matches the desired world pose + assert_vec3_close(Gf.Vec3d(*child_world_pos), world_position, eps=1e-5) + assert_quat_close(Gf.Quatd(*child_world_quat), world_orientation, eps=1e-5) + + +def test_convert_world_pose_to_local_with_scale(): + """Test world-to-local conversion with parent scale.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create parent with non-uniform scale + parent_prim = sim_utils.create_prim( + "/World/ScaledParent", + "Xform", + translation=(1.0, 2.0, 3.0), + orientation=(1.0, 0.0, 0.0, 0.0), + scale=(2.0, 2.0, 2.0), + stage=stage, + ) + + # World pose we want + world_position = (5.0, 6.0, 7.0) + world_orientation = (0.7071068, 0.7071068, 0.0, 0.0) # 90 deg around X + + # Convert to local space + local_translation, local_orientation = sim_utils.convert_world_pose_to_local( + world_position, world_orientation, parent_prim + ) + + # Create child and verify + child_prim = sim_utils.create_prim( + "/World/ScaledParent/Child", + "Xform", + translation=local_translation, + orientation=local_orientation, + stage=stage, + ) + + # Get world pose + child_world_pos, child_world_quat = sim_utils.resolve_prim_pose(child_prim) + + # Verify (may have some tolerance due to scale effects on rotation) + assert_vec3_close(Gf.Vec3d(*child_world_pos), world_position, eps=1e-4) + assert_quat_close(Gf.Quatd(*child_world_quat), world_orientation, eps=1e-4) + + +def test_convert_world_pose_to_local_invalid_parent(): + """Test world-to-local conversion with invalid parent returns world pose unchanged.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Get an invalid prim + invalid_prim = stage.GetPrimAtPath("/World/NonExistent") + assert not invalid_prim.IsValid() + + world_position = (10.0, 20.0, 30.0) + world_orientation = (0.7071068, 0.0, 0.7071068, 0.0) + + # Convert with invalid reference prim + with pytest.raises(ValueError): + sim_utils.convert_world_pose_to_local(world_position, world_orientation, invalid_prim) + + +def test_convert_world_pose_to_local_root_parent(): + """Test world-to-local conversion with root as parent returns world pose unchanged.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Get the pseudo-root prim + root_prim = stage.GetPrimAtPath("/") + + world_position = (15.0, 25.0, 35.0) + world_orientation = (0.9238795, 0.3826834, 0.0, 0.0) + + # Convert with root parent + local_translation, local_orientation = sim_utils.convert_world_pose_to_local( + world_position, world_orientation, root_prim + ) + # Assert orientation is not None + assert local_orientation is not None + + # Should return unchanged + assert_vec3_close(Gf.Vec3d(*local_translation), world_position, eps=1e-10) + assert_quat_close(Gf.Quatd(*local_orientation), world_orientation, eps=1e-10) + + +def test_convert_world_pose_to_local_none_orientation(): + """Test world-to-local conversion with None orientation.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create parent + parent_prim = sim_utils.create_prim( + "/World/ParentNoOrient", + "Xform", + translation=(3.0, 4.0, 5.0), + orientation=(0.7071068, 0.0, 0.0, 0.7071068), # 90 deg around Z + stage=stage, + ) + + world_position = (10.0, 10.0, 10.0) + + # Convert with None orientation + local_translation, local_orientation = sim_utils.convert_world_pose_to_local(world_position, None, parent_prim) + + # Orientation should be None + assert local_orientation is None + # Translation should still be converted + assert local_translation is not None + + +def test_convert_world_pose_to_local_complex_hierarchy(): + """Test world-to-local conversion in a complex hierarchy.""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a complex hierarchy + _ = sim_utils.create_prim( + "/World/Grandparent", + "Xform", + translation=(10.0, 0.0, 0.0), + orientation=(0.7071068, 0.0, 0.0, 0.7071068), # 90 deg around Z + scale=(2.0, 2.0, 2.0), + stage=stage, + ) + + parent = sim_utils.create_prim( + "/World/Grandparent/Parent", + "Xform", + translation=(5.0, 0.0, 0.0), # local to grandparent + orientation=(0.7071068, 0.7071068, 0.0, 0.0), # 90 deg around X + scale=(0.5, 0.5, 0.5), + stage=stage, + ) + + # World pose we want to achieve + world_position = (20.0, 15.0, 10.0) + world_orientation = (1.0, 0.0, 0.0, 0.0) + + # Convert to local space relative to parent + local_translation, local_orientation = sim_utils.convert_world_pose_to_local( + world_position, world_orientation, parent + ) + + # Create child with the computed local transform + child = sim_utils.create_prim( + "/World/Grandparent/Parent/Child", + "Xform", + translation=local_translation, + orientation=local_orientation, + stage=stage, + ) + + # Verify world pose + child_world_pos, child_world_quat = sim_utils.resolve_prim_pose(child) + + # Should match the desired world pose (with some tolerance for complex transforms) + assert_vec3_close(Gf.Vec3d(*child_world_pos), world_position, eps=1e-4) + assert_quat_close(Gf.Quatd(*child_world_quat), world_orientation, eps=1e-4) + + +def test_convert_world_pose_to_local_with_mixed_prim_types(): + """Test world-to-local conversion with mixed prim types (Xform, Scope, Mesh).""" + # obtain stage handle + stage = sim_utils.get_current_stage() + + # Create a hierarchy with different prim types + # Grandparent: Xform with transform + sim_utils.create_prim( + "/World/Grandparent", + "Xform", + translation=(5.0, 3.0, 2.0), + orientation=(0.7071068, 0.0, 0.0, 0.7071068), # 90 deg around Z + scale=(2.0, 2.0, 2.0), + stage=stage, + ) + + # Parent: Scope prim (organizational, typically has no transform) + parent = stage.DefinePrim("/World/Grandparent/Parent", "Scope") + + # Obtain parent prim pose (should be grandparent's transform) + parent_pos, parent_quat = sim_utils.resolve_prim_pose(parent) + assert_vec3_close(Gf.Vec3d(*parent_pos), (5.0, 3.0, 2.0), eps=1e-5) + assert_quat_close(Gf.Quatd(*parent_quat), (0.7071068, 0.0, 0.0, 0.7071068), eps=1e-5) + + # Child: Mesh prim (geometry) + child = sim_utils.create_prim("/World/Grandparent/Parent/Child", "Mesh", stage=stage) + + # World pose we want to achieve for the child + world_position = (10.0, 5.0, 3.0) + world_orientation = (1.0, 0.0, 0.0, 0.0) # identity rotation + + # Convert to local space relative to parent (Scope) + local_translation, local_orientation = sim_utils.convert_world_pose_to_local( + world_position, world_orientation, child + ) + + # Verify orientation is not None + assert local_orientation is not None, "Expected orientation to be computed" + + # Set the local transform on the child (Mesh) + xformable = UsdGeom.Xformable(child) + translate_op = xformable.GetTranslateOp() + translate_op.Set(Gf.Vec3d(*local_translation)) + orient_op = xformable.GetOrientOp() + orient_op.Set(Gf.Quatd(*local_orientation)) + + # Verify world pose of child + child_world_pos, child_world_quat = sim_utils.resolve_prim_pose(child) + + # Should match the desired world pose + # Note: Scope prims typically have no transform, so the child's world pose should account + # for the grandparent's transform + assert_vec3_close(Gf.Vec3d(*child_world_pos), world_position, eps=1e-10) + assert_quat_close(Gf.Quatd(*child_world_quat), world_orientation, eps=1e-10) diff --git a/source/isaaclab/test/sim/test_views_xform_prim.py b/source/isaaclab/test/sim/test_views_xform_prim.py new file mode 100644 index 000000000000..94b49a560bc3 --- /dev/null +++ b/source/isaaclab/test/sim/test_views_xform_prim.py @@ -0,0 +1,1500 @@ +# 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 + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import pytest # noqa: E402 +import torch # noqa: E402 + +try: + from isaacsim.core.prims import XFormPrim as _IsaacSimXformPrimView +except (ModuleNotFoundError, ImportError): + _IsaacSimXformPrimView = None + +import isaaclab.sim as sim_utils # noqa: E402 +from isaaclab.sim.views import XformPrimView as XformPrimView # noqa: E402 +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR # noqa: E402 + + +@pytest.fixture(autouse=True) +def test_setup_teardown(): + """Create a blank new stage for each test.""" + # Setup: Create a new stage + sim_utils.create_new_stage() + sim_utils.update_stage() + + # Yield for the test + yield + + # Teardown: Clear stage after each test + sim_utils.clear_stage() + sim_utils.SimulationContext.clear_instance() + + +""" +Helper functions. +""" + + +def _prepare_indices(index_type, target_indices, num_prims, device): + """Helper function to prepare indices based on type.""" + if index_type == "list": + return target_indices, target_indices + elif index_type == "torch_tensor": + return torch.tensor(target_indices, dtype=torch.int64, device=device), target_indices + elif index_type == "slice_none": + return slice(None), list(range(num_prims)) + else: + raise ValueError(f"Unknown index type: {index_type}") + + +def _skip_if_backend_unavailable(backend: str, device: str): + """Skip tests when the requested backend is unavailable.""" + if device.startswith("cuda") and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + if backend == "fabric" and device == "cpu": + pytest.skip("Warp fabricarray operations on CPU have known issues") + + +def _prim_type_for_backend(backend: str) -> str: + """Return a prim type that is compatible with the backend.""" + return "Camera" if backend == "fabric" else "Xform" + + +def _create_view(pattern: str, device: str, backend: str) -> XformPrimView: + """Create an XformPrimView for the requested backend.""" + if backend == "fabric": + sim_utils.SimulationContext(sim_utils.SimulationCfg(dt=0.01, device=device, use_fabric=True)) + return XformPrimView(pattern, device=device) + + +""" +Tests - Initialization. +""" + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_xform_prim_view_initialization_single_prim(device): + """Test XformPrimView initialization with a single prim.""" + # check if CUDA is available + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + # Create a single xform prim + stage = sim_utils.get_current_stage() + sim_utils.create_prim("/World/Object", "Xform", translation=(1.0, 2.0, 3.0), stage=stage) + + # Create view + view = XformPrimView("/World/Object", device=device) + + # Verify properties + assert view.count == 1 + assert view.prim_paths == ["/World/Object"] + assert view.device == device + assert len(view.prims) == 1 + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_xform_prim_view_initialization_multiple_prims(device): + """Test XformPrimView initialization with multiple prims using pattern matching.""" + # check if CUDA is available + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + # Create multiple prims + num_prims = 10 + stage = sim_utils.get_current_stage() + for i in range(num_prims): + sim_utils.create_prim(f"/World/Env_{i}/Object", "Xform", translation=(i * 2.0, 0.0, 1.0), stage=stage) + + # Create view with pattern + view = XformPrimView("/World/Env_.*/Object", device=device) + + # Verify properties + assert view.count == num_prims + assert view.device == device + assert len(view.prims) == num_prims + assert view.prim_paths == [f"/World/Env_{i}/Object" for i in range(num_prims)] + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_xform_prim_view_initialization_multiple_prims_order(device): + """Test XformPrimView initialization with multiple prims using pattern matching with multiple objects per prim. + + This test validates that XformPrimView respects USD stage traversal order, which is based on + creation order (depth-first search), NOT alphabetical/lexical sorting. This is an important + edge case that ensures deterministic prim ordering that matches USD's internal representation. + + The test creates prims in a deliberately non-alphabetical order (1, 0, A, a, 2) and verifies + that they are retrieved in creation order, not sorted order (0, 1, 2, A, a). + """ + # check if CUDA is available + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + # Create multiple prims + num_prims = 10 + stage = sim_utils.get_current_stage() + + # NOTE: Prims are created in a specific order to test that XformPrimView respects + # USD stage traversal order (DFS based on creation order), NOT alphabetical/lexical order. + # This is an important edge case: children under the same parent are returned in the + # order they were created, not sorted by name. + + # First batch: Create Object_1, Object_0, Object_A for each environment + # (intentionally non-alphabetical: 1, 0, A instead of 0, 1, A) + for i in range(num_prims): + sim_utils.create_prim(f"/World/Env_{i}/Object_1", "Xform", translation=(i * 2.0, -2.0, 1.0), stage=stage) + sim_utils.create_prim(f"/World/Env_{i}/Object_0", "Xform", translation=(i * 2.0, 2.0, 1.0), stage=stage) + sim_utils.create_prim(f"/World/Env_{i}/Object_A", "Xform", translation=(i * 2.0, 0.0, -1.0), stage=stage) + + # Second batch: Create Object_a, Object_2 for each environment + # (created after the first batch to verify traversal is depth-first per environment) + for i in range(num_prims): + sim_utils.create_prim(f"/World/Env_{i}/Object_a", "Xform", translation=(i * 2.0, 2.0, -1.0), stage=stage) + sim_utils.create_prim(f"/World/Env_{i}/Object_2", "Xform", translation=(i * 2.0, 2.0, 1.0), stage=stage) + + # Create view with pattern + view = XformPrimView("/World/Env_.*/Object_.*", device=device) + + # Expected ordering: DFS traversal by environment, with children in creation order + # For each Env_i, we expect: Object_1, Object_0, Object_A, Object_a, Object_2 + # (matches creation order, NOT alphabetical: would be 0, 1, 2, A, a if sorted) + expected_prim_paths_ordering = [] + for i in range(num_prims): + expected_prim_paths_ordering.append(f"/World/Env_{i}/Object_1") + expected_prim_paths_ordering.append(f"/World/Env_{i}/Object_0") + expected_prim_paths_ordering.append(f"/World/Env_{i}/Object_A") + expected_prim_paths_ordering.append(f"/World/Env_{i}/Object_a") + expected_prim_paths_ordering.append(f"/World/Env_{i}/Object_2") + + # Verify properties + assert view.count == num_prims * 5 + assert view.device == device + assert len(view.prims) == num_prims * 5 + assert view.prim_paths == expected_prim_paths_ordering + + # Additional validation: Verify ordering is NOT alphabetical + # If it were alphabetical, Object_0 would come before Object_1 + alphabetical_order = [] + for i in range(num_prims): + alphabetical_order.append(f"/World/Env_{i}/Object_0") + alphabetical_order.append(f"/World/Env_{i}/Object_1") + alphabetical_order.append(f"/World/Env_{i}/Object_2") + alphabetical_order.append(f"/World/Env_{i}/Object_A") + alphabetical_order.append(f"/World/Env_{i}/Object_a") + + assert view.prim_paths != alphabetical_order, ( + "Prim paths should follow creation order, not alphabetical order. " + "This test validates that USD stage traversal respects creation order." + ) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_xform_prim_view_initialization_invalid_prim(device): + """Test XformPrimView initialization fails for non-xformable prims.""" + # check if CUDA is available + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create a prim with non-standard xform operations + stage.DefinePrim("/World/InvalidPrim", "Xform") + + # XformPrimView should raise ValueError because prim doesn't have standard operations + with pytest.raises(ValueError, match="not a xformable prim"): + XformPrimView("/World/InvalidPrim", device=device) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_xform_prim_view_initialization_empty_pattern(device): + """Test XformPrimView initialization with pattern that matches no prims.""" + # check if CUDA is available + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + sim_utils.create_new_stage() + + # Create view with pattern that matches nothing + view = XformPrimView("/World/NonExistent_.*", device=device) + + # Should have zero count + assert view.count == 0 + assert len(view.prims) == 0 + + +""" +Tests - Getters. +""" + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_get_world_poses(device, backend): + """Test getting world poses from XformPrimView.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims with known world poses + expected_positions = [(1.0, 2.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.0)] + expected_orientations = [(1.0, 0.0, 0.0, 0.0), (0.7071068, 0.0, 0.0, 0.7071068), (0.7071068, 0.7071068, 0.0, 0.0)] + + for i, (pos, quat) in enumerate(zip(expected_positions, expected_orientations)): + sim_utils.create_prim(f"/World/Object_{i}", prim_type, translation=pos, orientation=quat, stage=stage) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Convert expected values to tensors + expected_positions_tensor = torch.tensor(expected_positions, dtype=torch.float32, device=device) + expected_orientations_tensor = torch.tensor(expected_orientations, dtype=torch.float32, device=device) + + # Get world poses + positions, orientations = view.get_world_poses() + + # Verify shapes + assert positions.shape == (3, 3) + assert orientations.shape == (3, 4) + + # Verify positions + torch.testing.assert_close(positions, expected_positions_tensor, atol=1e-5, rtol=0) + + # Verify orientations (allow for quaternion sign ambiguity) + try: + torch.testing.assert_close(orientations, expected_orientations_tensor, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(orientations, -expected_orientations_tensor, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_get_local_poses(device, backend): + """Test getting local poses from XformPrimView.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create parent and child prims + sim_utils.create_prim("/World/Parent", "Xform", translation=(10.0, 0.0, 0.0), stage=stage) + + # Children with different local poses + expected_local_positions = [(1.0, 0.0, 0.0), (0.0, 2.0, 0.0), (0.0, 0.0, 3.0)] + expected_local_orientations = [ + (1.0, 0.0, 0.0, 0.0), + (0.7071068, 0.0, 0.0, 0.7071068), + (0.7071068, 0.7071068, 0.0, 0.0), + ] + + for i, (pos, quat) in enumerate(zip(expected_local_positions, expected_local_orientations)): + sim_utils.create_prim(f"/World/Parent/Child_{i}", prim_type, translation=pos, orientation=quat, stage=stage) + + # Create view + view = _create_view("/World/Parent/Child_.*", device=device, backend=backend) + + # Get local poses + translations, orientations = view.get_local_poses() + + # Verify shapes + assert translations.shape == (3, 3) + assert orientations.shape == (3, 4) + + # Convert expected values to tensors + expected_translations_tensor = torch.tensor(expected_local_positions, dtype=torch.float32, device=device) + expected_orientations_tensor = torch.tensor(expected_local_orientations, dtype=torch.float32, device=device) + + # Verify translations + torch.testing.assert_close(translations, expected_translations_tensor, atol=1e-5, rtol=0) + + # Verify orientations (allow for quaternion sign ambiguity) + try: + torch.testing.assert_close(orientations, expected_orientations_tensor, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(orientations, -expected_orientations_tensor, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_get_scales(device, backend): + """Test getting scales from XformPrimView.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims with different scales + expected_scales = [(1.0, 1.0, 1.0), (2.0, 2.0, 2.0), (1.0, 2.0, 3.0)] + + for i, scale in enumerate(expected_scales): + sim_utils.create_prim(f"/World/Object_{i}", prim_type, scale=scale, stage=stage) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + expected_scales_tensor = torch.tensor(expected_scales, dtype=torch.float32, device=device) + + # Get scales + scales = view.get_scales() + + # Verify shape and values + assert scales.shape == (3, 3) + torch.testing.assert_close(scales, expected_scales_tensor, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_get_visibility(device): + """Test getting visibility when all prims are visible.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create prims (default is visible) + num_prims = 5 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", "Xform", translation=(float(i), 0.0, 0.0), stage=stage) + + # Create view + view = XformPrimView("/World/Object_.*", device=device) + + # Get visibility + visibility = view.get_visibility() + + # Verify shape and values + assert visibility.shape == (num_prims,) + assert visibility.dtype == torch.bool + assert torch.all(visibility), "All prims should be visible by default" + + +""" +Tests - Setters. +""" + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_set_world_poses(device, backend): + """Test setting world poses in XformPrimView.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims + num_prims = 5 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", prim_type, translation=(0.0, 0.0, 0.0), stage=stage) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Set new world poses + new_positions = torch.tensor( + [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], [10.0, 11.0, 12.0], [13.0, 14.0, 15.0]], device=device + ) + new_orientations = torch.tensor( + [ + [1.0, 0.0, 0.0, 0.0], + [0.7071068, 0.0, 0.0, 0.7071068], + [0.7071068, 0.7071068, 0.0, 0.0], + [0.9238795, 0.3826834, 0.0, 0.0], + [0.7071068, 0.0, 0.7071068, 0.0], + ], + device=device, + ) + + view.set_world_poses(new_positions, new_orientations) + + # Get the poses back + retrieved_positions, retrieved_orientations = view.get_world_poses() + + # Verify they match + torch.testing.assert_close(retrieved_positions, new_positions, atol=1e-5, rtol=0) + # Check quaternions (allow sign flip) + try: + torch.testing.assert_close(retrieved_orientations, new_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(retrieved_orientations, -new_orientations, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_set_world_poses_only_positions(device, backend): + """Test setting only positions, leaving orientations unchanged.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims with specific orientations + initial_quat = (0.7071068, 0.0, 0.0, 0.7071068) # 90 deg around Z + for i in range(3): + sim_utils.create_prim( + f"/World/Object_{i}", prim_type, translation=(0.0, 0.0, 0.0), orientation=initial_quat, stage=stage + ) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Get initial orientations + _, initial_orientations = view.get_world_poses() + + # Set only positions + new_positions = torch.tensor([[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]], device=device) + view.set_world_poses(positions=new_positions, orientations=None) + + # Get poses back + retrieved_positions, retrieved_orientations = view.get_world_poses() + + # Positions should be updated + torch.testing.assert_close(retrieved_positions, new_positions, atol=1e-5, rtol=0) + + # Orientations should be unchanged + try: + torch.testing.assert_close(retrieved_orientations, initial_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(retrieved_orientations, -initial_orientations, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_set_world_poses_only_orientations(device, backend): + """Test setting only orientations, leaving positions unchanged.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims with specific positions + for i in range(3): + sim_utils.create_prim(f"/World/Object_{i}", prim_type, translation=(float(i), 0.0, 0.0), stage=stage) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Get initial positions + initial_positions, _ = view.get_world_poses() + + # Set only orientations + new_orientations = torch.tensor( + [[0.7071068, 0.0, 0.0, 0.7071068], [0.7071068, 0.7071068, 0.0, 0.0], [0.9238795, 0.3826834, 0.0, 0.0]], + device=device, + ) + view.set_world_poses(positions=None, orientations=new_orientations) + + # Get poses back + retrieved_positions, retrieved_orientations = view.get_world_poses() + + # Positions should be unchanged + torch.testing.assert_close(retrieved_positions, initial_positions, atol=1e-5, rtol=0) + + # Orientations should be updated + try: + torch.testing.assert_close(retrieved_orientations, new_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(retrieved_orientations, -new_orientations, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_set_world_poses_with_hierarchy(device, backend): + """Test setting world poses correctly handles parent transformations.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + child_prim_type = _prim_type_for_backend(backend) + + # Create parent prims + for i in range(3): + parent_pos = (i * 10.0, 0.0, 0.0) + parent_quat = (0.7071068, 0.0, 0.0, 0.7071068) # 90 deg around Z + sim_utils.create_prim( + f"/World/Parent_{i}", "Xform", translation=parent_pos, orientation=parent_quat, stage=stage + ) + # Create child prims + sim_utils.create_prim(f"/World/Parent_{i}/Child", child_prim_type, translation=(0.0, 0.0, 0.0), stage=stage) + + # Create view for children + view = _create_view("/World/Parent_.*/Child", device=device, backend=backend) + + # Set world poses for children + desired_world_positions = torch.tensor([[5.0, 5.0, 0.0], [15.0, 5.0, 0.0], [25.0, 5.0, 0.0]], device=device) + desired_world_orientations = torch.tensor( + [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], device=device + ) + + view.set_world_poses(desired_world_positions, desired_world_orientations) + + # Get world poses back + retrieved_positions, retrieved_orientations = view.get_world_poses() + + # Should match desired world poses + torch.testing.assert_close(retrieved_positions, desired_world_positions, atol=1e-4, rtol=0) + try: + torch.testing.assert_close(retrieved_orientations, desired_world_orientations, atol=1e-4, rtol=0) + except AssertionError: + torch.testing.assert_close(retrieved_orientations, -desired_world_orientations, atol=1e-4, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_set_local_poses(device, backend): + """Test setting local poses in XformPrimView.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create parent + sim_utils.create_prim("/World/Parent", "Xform", translation=(5.0, 5.0, 5.0), stage=stage) + + # Create children + num_prims = 4 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Parent/Child_{i}", prim_type, translation=(0.0, 0.0, 0.0), stage=stage) + + # Create view + view = _create_view("/World/Parent/Child_.*", device=device, backend=backend) + + # Set new local poses + new_translations = torch.tensor([[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0], [4.0, 4.0, 4.0]], device=device) + new_orientations = torch.tensor( + [ + [1.0, 0.0, 0.0, 0.0], + [0.7071068, 0.0, 0.0, 0.7071068], + [0.7071068, 0.7071068, 0.0, 0.0], + [0.9238795, 0.3826834, 0.0, 0.0], + ], + device=device, + ) + + view.set_local_poses(new_translations, new_orientations) + + # Get local poses back + retrieved_translations, retrieved_orientations = view.get_local_poses() + + # Verify they match + torch.testing.assert_close(retrieved_translations, new_translations, atol=1e-5, rtol=0) + try: + torch.testing.assert_close(retrieved_orientations, new_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(retrieved_orientations, -new_orientations, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_set_local_poses_only_translations(device, backend): + """Test setting only local translations.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create parent and children with specific orientations + sim_utils.create_prim("/World/Parent", "Xform", translation=(0.0, 0.0, 0.0), stage=stage) + initial_quat = (0.7071068, 0.0, 0.0, 0.7071068) + + for i in range(3): + sim_utils.create_prim( + f"/World/Parent/Child_{i}", + prim_type, + translation=(0.0, 0.0, 0.0), + orientation=initial_quat, + stage=stage, + ) + + # Create view + view = _create_view("/World/Parent/Child_.*", device=device, backend=backend) + + # Get initial orientations + _, initial_orientations = view.get_local_poses() + + # Set only translations + new_translations = torch.tensor([[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]], device=device) + view.set_local_poses(translations=new_translations, orientations=None) + + # Get poses back + retrieved_translations, retrieved_orientations = view.get_local_poses() + + # Translations should be updated + torch.testing.assert_close(retrieved_translations, new_translations, atol=1e-5, rtol=0) + + # Orientations should be unchanged + try: + torch.testing.assert_close(retrieved_orientations, initial_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(retrieved_orientations, -initial_orientations, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_set_scales(device, backend): + """Test setting scales in XformPrimView.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims + num_prims = 5 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", prim_type, scale=(1.0, 1.0, 1.0), stage=stage) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Set new scales + new_scales = torch.tensor( + [[2.0, 2.0, 2.0], [1.0, 2.0, 3.0], [0.5, 0.5, 0.5], [3.0, 1.0, 2.0], [1.5, 1.5, 1.5]], device=device + ) + + view.set_scales(new_scales) + + # Get scales back + retrieved_scales = view.get_scales() + + # Verify they match + torch.testing.assert_close(retrieved_scales, new_scales, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_set_visibility(device): + """Test toggling visibility multiple times.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create prims + num_prims = 3 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", "Xform", stage=stage) + + # Create view + view = XformPrimView("/World/Object_.*", device=device) + + # Initial state: all visible + visibility = view.get_visibility() + assert torch.all(visibility), "All should be visible initially" + + # Make all invisible + view.set_visibility(torch.zeros(num_prims, dtype=torch.bool, device=device)) + visibility = view.get_visibility() + assert not torch.any(visibility), "All should be invisible" + + # Make all visible again + view.set_visibility(torch.ones(num_prims, dtype=torch.bool, device=device)) + visibility = view.get_visibility() + assert torch.all(visibility), "All should be visible again" + + # Toggle individual prims + view.set_visibility(torch.tensor([False], dtype=torch.bool, device=device), indices=[1]) + visibility = view.get_visibility() + assert visibility[0] and not visibility[1] and visibility[2], "Only middle prim should be invisible" + + +""" +Tests - Index Handling. +""" + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("index_type", ["list", "torch_tensor", "slice_none"]) +@pytest.mark.parametrize("method", ["world_poses", "local_poses", "scales", "visibility"]) +def test_index_types_get_methods(device, index_type, method): + """Test that getter methods work with different index types.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create prims based on method type + num_prims = 10 + if method == "local_poses": + # Create parent and children for local poses + sim_utils.create_prim("/World/Parent", "Xform", translation=(10.0, 0.0, 0.0), stage=stage) + for i in range(num_prims): + sim_utils.create_prim( + f"/World/Parent/Child_{i}", "Xform", translation=(float(i), float(i) * 0.5, 0.0), stage=stage + ) + view = XformPrimView("/World/Parent/Child_.*", device=device) + elif method == "scales": + # Create prims with different scales + for i in range(num_prims): + scale = (1.0 + i * 0.5, 1.0 + i * 0.3, 1.0 + i * 0.2) + sim_utils.create_prim(f"/World/Object_{i}", "Xform", scale=scale, stage=stage) + view = XformPrimView("/World/Object_.*", device=device) + else: # world_poses + # Create prims with different positions + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", "Xform", translation=(float(i), 0.0, 0.0), stage=stage) + view = XformPrimView("/World/Object_.*", device=device) + + # Get all data as reference + if method == "world_poses": + all_data1, all_data2 = view.get_world_poses() + elif method == "local_poses": + all_data1, all_data2 = view.get_local_poses() + elif method == "scales": + all_data1 = view.get_scales() + all_data2 = None + else: # visibility + all_data1 = view.get_visibility() + all_data2 = None + + # Prepare indices + target_indices_base = [2, 5, 7] + indices, target_indices = _prepare_indices(index_type, target_indices_base, num_prims, device) + + # Get subset + if method == "world_poses": + subset_data1, subset_data2 = view.get_world_poses(indices=indices) # type: ignore[arg-type] + elif method == "local_poses": + subset_data1, subset_data2 = view.get_local_poses(indices=indices) # type: ignore[arg-type] + elif method == "scales": + subset_data1 = view.get_scales(indices=indices) # type: ignore[arg-type] + subset_data2 = None + else: # visibility + subset_data1 = view.get_visibility(indices=indices) # type: ignore[arg-type] + subset_data2 = None + + # Verify shapes + expected_count = len(target_indices) + if method == "visibility": + assert subset_data1.shape == (expected_count,) + else: + assert subset_data1.shape == (expected_count, 3) + if subset_data2 is not None: + assert subset_data2.shape == (expected_count, 4) + + # Verify values + target_indices_tensor = torch.tensor(target_indices, dtype=torch.int64, device=device) + torch.testing.assert_close(subset_data1, all_data1[target_indices_tensor], atol=1e-5, rtol=0) + if subset_data2 is not None and all_data2 is not None: + torch.testing.assert_close(subset_data2, all_data2[target_indices_tensor], atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("index_type", ["list", "torch_tensor", "slice_none"]) +@pytest.mark.parametrize("method", ["world_poses", "local_poses", "scales", "visibility"]) +def test_index_types_set_methods(device, index_type, method): + """Test that setter methods work with different index types.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create prims based on method type + num_prims = 10 + if method == "local_poses": + # Create parent and children for local poses + sim_utils.create_prim("/World/Parent", "Xform", translation=(5.0, 5.0, 0.0), stage=stage) + for i in range(num_prims): + sim_utils.create_prim(f"/World/Parent/Child_{i}", "Xform", translation=(float(i), 0.0, 0.0), stage=stage) + view = XformPrimView("/World/Parent/Child_.*", device=device) + else: # world_poses or scales + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", "Xform", translation=(0.0, 0.0, 0.0), stage=stage) + view = XformPrimView("/World/Object_.*", device=device) + + # Get initial data + if method == "world_poses": + initial_data1, initial_data2 = view.get_world_poses() + elif method == "local_poses": + initial_data1, initial_data2 = view.get_local_poses() + elif method == "scales": + initial_data1 = view.get_scales() + initial_data2 = None + else: # visibility + initial_data1 = view.get_visibility() + initial_data2 = None + + # Prepare indices + target_indices_base = [2, 5, 7] + indices, target_indices = _prepare_indices(index_type, target_indices_base, num_prims, device) + + # Prepare new data + num_to_set = len(target_indices) + if method in ["world_poses", "local_poses"]: + new_data1 = torch.randn(num_to_set, 3, device=device) * 10.0 + new_data2 = torch.tensor([[1.0, 0.0, 0.0, 0.0]] * num_to_set, dtype=torch.float32, device=device) + elif method == "scales": + new_data1 = torch.rand(num_to_set, 3, device=device) * 2.0 + 0.5 + new_data2 = None + else: # visibility + # Set to False to test change (default is True) + new_data1 = torch.zeros(num_to_set, dtype=torch.bool, device=device) + new_data2 = None + + # Set data + if method == "world_poses": + view.set_world_poses(positions=new_data1, orientations=new_data2, indices=indices) # type: ignore[arg-type] + elif method == "local_poses": + view.set_local_poses(translations=new_data1, orientations=new_data2, indices=indices) # type: ignore[arg-type] + elif method == "scales": + view.set_scales(scales=new_data1, indices=indices) # type: ignore[arg-type] + else: # visibility + view.set_visibility(visibility=new_data1, indices=indices) # type: ignore[arg-type] + + # Get all data after update + if method == "world_poses": + updated_data1, updated_data2 = view.get_world_poses() + elif method == "local_poses": + updated_data1, updated_data2 = view.get_local_poses() + elif method == "scales": + updated_data1 = view.get_scales() + updated_data2 = None + else: # visibility + updated_data1 = view.get_visibility() + updated_data2 = None + + # Verify that specified indices were updated + for i, target_idx in enumerate(target_indices): + torch.testing.assert_close(updated_data1[target_idx], new_data1[i], atol=1e-5, rtol=0) + if new_data2 is not None and updated_data2 is not None: + try: + torch.testing.assert_close(updated_data2[target_idx], new_data2[i], atol=1e-5, rtol=0) + except AssertionError: + # Account for quaternion sign ambiguity + torch.testing.assert_close(updated_data2[target_idx], -new_data2[i], atol=1e-5, rtol=0) + + # Verify that other indices were NOT updated (only for non-slice(None) cases) + if index_type != "slice_none": + for i in range(num_prims): + if i not in target_indices: + torch.testing.assert_close(updated_data1[i], initial_data1[i], atol=1e-5, rtol=0) + if initial_data2 is not None and updated_data2 is not None: + try: + torch.testing.assert_close(updated_data2[i], initial_data2[i], atol=1e-5, rtol=0) + except AssertionError: + # Account for quaternion sign ambiguity + torch.testing.assert_close(updated_data2[i], -initial_data2[i], atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_indices_single_element(device, backend): + """Test with a single index.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims + num_prims = 5 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", prim_type, translation=(float(i), 0.0, 0.0), stage=stage) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Test with single index + indices = [3] + positions, orientations = view.get_world_poses(indices=indices) + + # Verify shapes + assert positions.shape == (1, 3) + assert orientations.shape == (1, 4) + + # Set pose for single index + new_position = torch.tensor([[100.0, 200.0, 300.0]], device=device) + view.set_world_poses(positions=new_position, indices=indices) + + # Verify it was set + retrieved_positions, _ = view.get_world_poses(indices=indices) + torch.testing.assert_close(retrieved_positions, new_position, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_indices_out_of_order(device, backend): + """Test with indices provided in non-sequential order.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims + num_prims = 10 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", prim_type, translation=(0.0, 0.0, 0.0), stage=stage) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Use out-of-order indices + indices = [7, 2, 9, 0, 5] + new_positions = torch.tensor( + [[7.0, 0.0, 0.0], [2.0, 0.0, 0.0], [9.0, 0.0, 0.0], [0.0, 0.0, 0.0], [5.0, 0.0, 0.0]], device=device + ) + + # Set poses with out-of-order indices + view.set_world_poses(positions=new_positions, indices=indices) + + # Get all poses + all_positions, _ = view.get_world_poses() + + # Verify each index got the correct value + expected_x_values = [0.0, 0.0, 2.0, 0.0, 0.0, 5.0, 0.0, 7.0, 0.0, 9.0] + for i in range(num_prims): + assert abs(all_positions[i, 0].item() - expected_x_values[i]) < 1e-5 + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("backend", ["usd", "fabric"]) +def test_indices_with_only_positions_or_orientations(device, backend): + """Test indices work correctly when setting only positions or only orientations.""" + _skip_if_backend_unavailable(backend, device) + + stage = sim_utils.get_current_stage() + prim_type = _prim_type_for_backend(backend) + + # Create prims + num_prims = 5 + for i in range(num_prims): + sim_utils.create_prim( + f"/World/Object_{i}", + prim_type, + translation=(0.0, 0.0, 0.0), + orientation=(1.0, 0.0, 0.0, 0.0), + stage=stage, + ) + + # Create view + view = _create_view("/World/Object_.*", device=device, backend=backend) + + # Get initial poses + initial_positions, initial_orientations = view.get_world_poses() + + # Set only positions for specific indices + indices = [1, 3] + new_positions = torch.tensor([[10.0, 0.0, 0.0], [30.0, 0.0, 0.0]], device=device) + view.set_world_poses(positions=new_positions, orientations=None, indices=indices) + + # Get updated poses + updated_positions, updated_orientations = view.get_world_poses() + + # Verify positions updated for indices 1 and 3, others unchanged + torch.testing.assert_close(updated_positions[1], new_positions[0], atol=1e-5, rtol=0) + torch.testing.assert_close(updated_positions[3], new_positions[1], atol=1e-5, rtol=0) + torch.testing.assert_close(updated_positions[0], initial_positions[0], atol=1e-5, rtol=0) + + # Verify all orientations unchanged + try: + torch.testing.assert_close(updated_orientations, initial_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(updated_orientations, -initial_orientations, atol=1e-5, rtol=0) + + # Now set only orientations for different indices + indices2 = [0, 4] + new_orientations = torch.tensor([[0.7071068, 0.0, 0.0, 0.7071068], [0.7071068, 0.7071068, 0.0, 0.0]], device=device) + view.set_world_poses(positions=None, orientations=new_orientations, indices=indices2) + + # Get final poses + final_positions, final_orientations = view.get_world_poses() + + # Verify positions unchanged from previous step + torch.testing.assert_close(final_positions, updated_positions, atol=1e-5, rtol=0) + + # Verify orientations updated for indices 0 and 4 + try: + torch.testing.assert_close(final_orientations[0], new_orientations[0], atol=1e-5, rtol=0) + torch.testing.assert_close(final_orientations[4], new_orientations[1], atol=1e-5, rtol=0) + except AssertionError: + # Account for quaternion sign ambiguity + torch.testing.assert_close(final_orientations[0], -new_orientations[0], atol=1e-5, rtol=0) + torch.testing.assert_close(final_orientations[4], -new_orientations[1], atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_index_type_none_equivalent_to_all(device): + """Test that indices=None is equivalent to getting/setting all prims.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create prims + num_prims = 6 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Object_{i}", "Xform", translation=(float(i), 0.0, 0.0), stage=stage) + + # Create view + view = XformPrimView("/World/Object_.*", device=device) + + # Get poses with indices=None + pos_none, quat_none = view.get_world_poses(indices=None) + + # Get poses with no argument (default) + pos_default, quat_default = view.get_world_poses() + + # Get poses with slice(None) + pos_slice, quat_slice = view.get_world_poses(indices=slice(None)) # type: ignore[arg-type] + + # All should be equivalent + torch.testing.assert_close(pos_none, pos_default, atol=1e-10, rtol=0) + torch.testing.assert_close(quat_none, quat_default, atol=1e-10, rtol=0) + torch.testing.assert_close(pos_none, pos_slice, atol=1e-10, rtol=0) + torch.testing.assert_close(quat_none, quat_slice, atol=1e-10, rtol=0) + + # Test the same for set operations + new_positions = torch.randn(num_prims, 3, device=device) * 10.0 + new_orientations = torch.tensor([[1.0, 0.0, 0.0, 0.0]] * num_prims, dtype=torch.float32, device=device) + + # Set with indices=None + view.set_world_poses(positions=new_positions, orientations=new_orientations, indices=None) + pos_after_none, quat_after_none = view.get_world_poses() + + # Reset + view.set_world_poses(positions=torch.zeros(num_prims, 3, device=device), indices=None) + + # Set with slice(None) + view.set_world_poses( + positions=new_positions, + orientations=new_orientations, + indices=slice(None), # type: ignore[arg-type] + ) + pos_after_slice, quat_after_slice = view.get_world_poses() + + # Should be equivalent + torch.testing.assert_close(pos_after_none, pos_after_slice, atol=1e-5, rtol=0) + torch.testing.assert_close(quat_after_none, quat_after_slice, atol=1e-5, rtol=0) + + +""" +Tests - Integration. +""" + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_with_franka_robots(device): + """Test XformPrimView with real Franka robot USD assets.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Load Franka robot assets + franka_usd_path = f"{ISAAC_NUCLEUS_DIR}/Robots/FrankaRobotics/FrankaPanda/franka.usd" + + # Add two Franka robots to the stage + sim_utils.create_prim("/World/Franka_1", "Xform", usd_path=franka_usd_path, stage=stage) + sim_utils.create_prim("/World/Franka_2", "Xform", usd_path=franka_usd_path, stage=stage) + + # Create view for both Frankas + frankas_view = XformPrimView("/World/Franka_.*", device=device) + + # Verify count + assert frankas_view.count == 2 + + # Get initial world poses (should be at origin) + initial_positions, initial_orientations = frankas_view.get_world_poses() + + # Verify initial positions are at origin + expected_initial_positions = torch.zeros(2, 3, device=device) + torch.testing.assert_close(initial_positions, expected_initial_positions, atol=1e-5, rtol=0) + + # Verify initial orientations are identity + expected_initial_orientations = torch.tensor([[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], device=device) + try: + torch.testing.assert_close(initial_orientations, expected_initial_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(initial_orientations, -expected_initial_orientations, atol=1e-5, rtol=0) + + # Set new world poses + new_positions = torch.tensor([[10.0, 10.0, 0.0], [-40.0, -40.0, 0.0]], device=device) + # 90° rotation around Z axis for first, -90° for second + new_orientations = torch.tensor( + [[0.7071068, 0.0, 0.0, 0.7071068], [0.7071068, 0.0, 0.0, -0.7071068]], device=device + ) + + frankas_view.set_world_poses(positions=new_positions, orientations=new_orientations) + + # Get poses back and verify + retrieved_positions, retrieved_orientations = frankas_view.get_world_poses() + + torch.testing.assert_close(retrieved_positions, new_positions, atol=1e-5, rtol=0) + try: + torch.testing.assert_close(retrieved_orientations, new_orientations, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(retrieved_orientations, -new_orientations, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_with_nested_targets(device): + """Test with nested frame/target structure similar to Isaac Sim tests.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create frames and targets + for i in range(1, 4): + sim_utils.create_prim(f"/World/Frame_{i}", "Xform", stage=stage) + sim_utils.create_prim(f"/World/Frame_{i}/Target", "Xform", stage=stage) + + # Create views + frames_view = XformPrimView("/World/Frame_.*", device=device) + targets_view = XformPrimView("/World/Frame_.*/Target", device=device) + + assert frames_view.count == 3 + assert targets_view.count == 3 + + # Set local poses for frames + frame_translations = torch.tensor([[0.0, 0.0, 0.0], [0.0, 10.0, 5.0], [0.0, 3.0, 5.0]], device=device) + frames_view.set_local_poses(translations=frame_translations) + + # Set local poses for targets + target_translations = torch.tensor([[0.0, 20.0, 10.0], [0.0, 30.0, 20.0], [0.0, 50.0, 10.0]], device=device) + targets_view.set_local_poses(translations=target_translations) + + # Get world poses of targets + world_positions, _ = targets_view.get_world_poses() + + # Expected world positions are frame_translation + target_translation + expected_positions = torch.tensor([[0.0, 20.0, 10.0], [0.0, 40.0, 25.0], [0.0, 53.0, 15.0]], device=device) + + torch.testing.assert_close(world_positions, expected_positions, atol=1e-5, rtol=0) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_visibility_with_hierarchy(device): + """Test visibility with parent-child hierarchy and inheritance.""" + if device == "cuda" and not torch.cuda.is_available(): + pytest.skip("CUDA not available") + + stage = sim_utils.get_current_stage() + + # Create parent and children + sim_utils.create_prim("/World/Parent", "Xform", stage=stage) + + num_children = 4 + for i in range(num_children): + sim_utils.create_prim(f"/World/Parent/Child_{i}", "Xform", stage=stage) + + # Create views for both parent and children + parent_view = XformPrimView("/World/Parent", device=device) + children_view = XformPrimView("/World/Parent/Child_.*", device=device) + + # Verify parent and all children are visible initially + parent_visibility = parent_view.get_visibility() + children_visibility = children_view.get_visibility() + assert parent_visibility[0], "Parent should be visible initially" + assert torch.all(children_visibility), "All children should be visible initially" + + # Make some children invisible directly + new_visibility = torch.tensor([True, False, True, False], dtype=torch.bool, device=device) + children_view.set_visibility(new_visibility) + + # Verify the visibility changes + retrieved_visibility = children_view.get_visibility() + torch.testing.assert_close(retrieved_visibility, new_visibility) + + # Make all children visible again + children_view.set_visibility(torch.ones(num_children, dtype=torch.bool, device=device)) + all_visible = children_view.get_visibility() + assert torch.all(all_visible), "All children should be visible again" + + # Now test parent visibility inheritance: + # Make parent invisible + parent_view.set_visibility(torch.tensor([False], dtype=torch.bool, device=device)) + + # Verify parent is invisible + parent_visibility = parent_view.get_visibility() + assert not parent_visibility[0], "Parent should be invisible" + + # Verify children are also invisible (due to parent being invisible) + children_visibility = children_view.get_visibility() + assert not torch.any(children_visibility), "All children should be invisible when parent is invisible" + + # Make parent visible again + parent_view.set_visibility(torch.tensor([True], dtype=torch.bool, device=device)) + + # Verify parent is visible + parent_visibility = parent_view.get_visibility() + assert parent_visibility[0], "Parent should be visible again" + + # Verify children are also visible again + children_visibility = children_view.get_visibility() + assert torch.all(children_visibility), "All children should be visible again when parent is visible" + + +""" +Tests - Comparison with Isaac Sim Implementation. +""" + + +def test_compare_get_world_poses_with_isaacsim(): + """Compare get_world_poses with Isaac Sim's implementation.""" + stage = sim_utils.get_current_stage() + + # Check if Isaac Sim is available + if _IsaacSimXformPrimView is None: + pytest.skip("Isaac Sim is not available") + + # Create prims with various poses + num_prims = 10 + for i in range(num_prims): + pos = (i * 2.0, i * 0.5, i * 1.5) + # Vary orientations + if i % 3 == 0: + quat = (1.0, 0.0, 0.0, 0.0) # Identity + elif i % 3 == 1: + quat = (0.7071068, 0.0, 0.0, 0.7071068) # 90 deg around Z + else: + quat = (0.7071068, 0.7071068, 0.0, 0.0) # 90 deg around X + sim_utils.create_prim(f"/World/Env_{i}/Object", "Xform", translation=pos, orientation=quat, stage=stage) + + pattern = "/World/Env_.*/Object" + + # Create both views + isaaclab_view = XformPrimView(pattern, device="cpu") + isaacsim_view = _IsaacSimXformPrimView(pattern, reset_xform_properties=False) + + # Get world poses from both + isaaclab_pos, isaaclab_quat = isaaclab_view.get_world_poses() + isaacsim_pos, isaacsim_quat = isaacsim_view.get_world_poses() + + # Convert Isaac Sim results to torch tensors if needed + if not isinstance(isaacsim_pos, torch.Tensor): + isaacsim_pos = torch.tensor(isaacsim_pos, dtype=torch.float32) + if not isinstance(isaacsim_quat, torch.Tensor): + isaacsim_quat = torch.tensor(isaacsim_quat, dtype=torch.float32) + + # Compare results + torch.testing.assert_close(isaaclab_pos, isaacsim_pos, atol=1e-5, rtol=0) + + # Compare quaternions (account for sign ambiguity) + try: + torch.testing.assert_close(isaaclab_quat, isaacsim_quat, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(isaaclab_quat, -isaacsim_quat, atol=1e-5, rtol=0) + + +def test_compare_set_world_poses_with_isaacsim(): + """Compare set_world_poses with Isaac Sim's implementation.""" + stage = sim_utils.get_current_stage() + + # Check if Isaac Sim is available + if _IsaacSimXformPrimView is None: + pytest.skip("Isaac Sim is not available") + + # Create prims + num_prims = 8 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Env_{i}/Object", "Xform", translation=(0.0, 0.0, 0.0), stage=stage) + + pattern = "/World/Env_.*/Object" + + # Create both views + isaaclab_view = XformPrimView(pattern, device="cpu") + isaacsim_view = _IsaacSimXformPrimView(pattern, reset_xform_properties=False) + + # Generate new poses + new_positions = torch.randn(num_prims, 3) * 10.0 + new_orientations = torch.tensor([[1.0, 0.0, 0.0, 0.0]] * num_prims, dtype=torch.float32) + + # Set poses using both implementations + isaaclab_view.set_world_poses(new_positions.clone(), new_orientations.clone()) + isaacsim_view.set_world_poses(new_positions.clone(), new_orientations.clone()) + + # Get poses back from both + isaaclab_pos, isaaclab_quat = isaaclab_view.get_world_poses() + isaacsim_pos, isaacsim_quat = isaacsim_view.get_world_poses() + + # Convert Isaac Sim results to torch tensors if needed + if not isinstance(isaacsim_pos, torch.Tensor): + isaacsim_pos = torch.tensor(isaacsim_pos, dtype=torch.float32) + if not isinstance(isaacsim_quat, torch.Tensor): + isaacsim_quat = torch.tensor(isaacsim_quat, dtype=torch.float32) + + # Compare results - both implementations should produce the same world poses + torch.testing.assert_close(isaaclab_pos, isaacsim_pos, atol=1e-4, rtol=0) + try: + torch.testing.assert_close(isaaclab_quat, isaacsim_quat, atol=1e-4, rtol=0) + except AssertionError: + torch.testing.assert_close(isaaclab_quat, -isaacsim_quat, atol=1e-4, rtol=0) + + +def test_compare_get_local_poses_with_isaacsim(): + """Compare get_local_poses with Isaac Sim's implementation.""" + stage = sim_utils.get_current_stage() + + # Check if Isaac Sim is available + if _IsaacSimXformPrimView is None: + pytest.skip("Isaac Sim is not available") + + # Create hierarchical prims + num_prims = 5 + for i in range(num_prims): + # Create parent + sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 5.0, 0.0, 0.0), stage=stage) + # Create child with local pose + local_pos = (1.0, float(i), 0.0) + local_quat = (1.0, 0.0, 0.0, 0.0) if i % 2 == 0 else (0.7071068, 0.0, 0.0, 0.7071068) + sim_utils.create_prim( + f"/World/Env_{i}/Object", "Xform", translation=local_pos, orientation=local_quat, stage=stage + ) + + pattern = "/World/Env_.*/Object" + + # Create both views + isaaclab_view = XformPrimView(pattern, device="cpu") + isaacsim_view = _IsaacSimXformPrimView(pattern, reset_xform_properties=False) + + # Get local poses from both + isaaclab_trans, isaaclab_quat = isaaclab_view.get_local_poses() + isaacsim_trans, isaacsim_quat = isaacsim_view.get_local_poses() + + # Convert Isaac Sim results to torch tensors if needed + if not isinstance(isaacsim_trans, torch.Tensor): + isaacsim_trans = torch.tensor(isaacsim_trans, dtype=torch.float32) + if not isinstance(isaacsim_quat, torch.Tensor): + isaacsim_quat = torch.tensor(isaacsim_quat, dtype=torch.float32) + + # Compare results + torch.testing.assert_close(isaaclab_trans, isaacsim_trans, atol=1e-5, rtol=0) + try: + torch.testing.assert_close(isaaclab_quat, isaacsim_quat, atol=1e-5, rtol=0) + except AssertionError: + torch.testing.assert_close(isaaclab_quat, -isaacsim_quat, atol=1e-5, rtol=0) + + +def test_compare_set_local_poses_with_isaacsim(): + """Compare set_local_poses with Isaac Sim's implementation.""" + stage = sim_utils.get_current_stage() + + # Check if Isaac Sim is available + if _IsaacSimXformPrimView is None: + pytest.skip("Isaac Sim is not available") + + # Create hierarchical prims + num_prims = 6 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0.0, 0.0), stage=stage) + sim_utils.create_prim(f"/World/Env_{i}/Object", "Xform", translation=(0.0, 0.0, 0.0), stage=stage) + + pattern = "/World/Env_.*/Object" + + # Create both views + isaaclab_view = XformPrimView(pattern, device="cpu") + isaacsim_view = _IsaacSimXformPrimView(pattern, reset_xform_properties=False) + + # Generate new local poses + new_translations = torch.randn(num_prims, 3) * 5.0 + new_orientations = torch.tensor( + [[1.0, 0.0, 0.0, 0.0], [0.7071068, 0.0, 0.0, 0.7071068]] * (num_prims // 2), dtype=torch.float32 + ) + + # Set local poses using both implementations + isaaclab_view.set_local_poses(new_translations.clone(), new_orientations.clone()) + isaacsim_view.set_local_poses(new_translations.clone(), new_orientations.clone()) + + # Get local poses back from both + isaaclab_trans, isaaclab_quat = isaaclab_view.get_local_poses() + isaacsim_trans, isaacsim_quat = isaacsim_view.get_local_poses() + + # Convert Isaac Sim results to torch tensors if needed + if not isinstance(isaacsim_trans, torch.Tensor): + isaacsim_trans = torch.tensor(isaacsim_trans, dtype=torch.float32) + if not isinstance(isaacsim_quat, torch.Tensor): + isaacsim_quat = torch.tensor(isaacsim_quat, dtype=torch.float32) + + # Compare results + torch.testing.assert_close(isaaclab_trans, isaacsim_trans, atol=1e-4, rtol=0) + try: + torch.testing.assert_close(isaaclab_quat, isaacsim_quat, atol=1e-4, rtol=0) + except AssertionError: + torch.testing.assert_close(isaaclab_quat, -isaacsim_quat, atol=1e-4, rtol=0) + + +""" +Tests - Fabric Operations. +""" + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_fabric_initialization(device): + """Test XformPrimView initialization with Fabric enabled.""" + _skip_if_backend_unavailable("fabric", device) + + stage = sim_utils.get_current_stage() + + # Create camera prims (Boundable prims that support Fabric) + num_prims = 5 + for i in range(num_prims): + sim_utils.create_prim(f"/World/Cam_{i}", "Camera", translation=(i * 1.0, 0.0, 1.0), stage=stage) + + # Create view with Fabric enabled + view = _create_view("/World/Cam_.*", device=device, backend="fabric") + + # Verify properties + assert view.count == num_prims + assert view.device == device + assert len(view.prims) == num_prims + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_fabric_usd_consistency(device): + """Test that Fabric round-trip (write→read) is consistent, matching Isaac Sim's design. + + Note: This does NOT test Fabric vs USD reads on initialization, as Fabric is designed + for write-first workflows. Instead, it tests that: + 1. Fabric write→read round-trip works correctly + 2. This matches Isaac Sim's Fabric behavior + """ + _skip_if_backend_unavailable("fabric", device) + + stage = sim_utils.get_current_stage() + + # Create prims + num_prims = 5 + for i in range(num_prims): + sim_utils.create_prim( + f"/World/Cam_{i}", + "Camera", + translation=(i * 1.0, 2.0, 3.0), + orientation=(0.7071068, 0.0, 0.0, 0.7071068), + stage=stage, + ) + + # Create Fabric view + view_fabric = _create_view("/World/Cam_.*", device=device, backend="fabric") + + # Test Fabric write→read round-trip (Isaac Sim's intended workflow) + # Initialize Fabric state by WRITING first + init_positions = torch.zeros((num_prims, 3), dtype=torch.float32, device=device) + init_positions[:, 0] = torch.arange(num_prims, dtype=torch.float32, device=device) + init_positions[:, 1] = 2.0 + init_positions[:, 2] = 3.0 + init_orientations = torch.tensor([[0.7071068, 0.0, 0.0, 0.7071068]] * num_prims, dtype=torch.float32, device=device) + + view_fabric.set_world_poses(init_positions, init_orientations) + + # Read back from Fabric (should match what we wrote) + pos_fabric, quat_fabric = view_fabric.get_world_poses() + torch.testing.assert_close(pos_fabric, init_positions, atol=1e-4, rtol=0) + torch.testing.assert_close(quat_fabric, init_orientations, atol=1e-4, rtol=0) + + # Test another round-trip with different values + new_positions = torch.rand((num_prims, 3), dtype=torch.float32, device=device) * 10.0 + new_orientations = torch.tensor([[1.0, 0.0, 0.0, 0.0]] * num_prims, dtype=torch.float32, device=device) + + view_fabric.set_world_poses(new_positions, new_orientations) + + # Read back from Fabric (should match) + pos_fabric_after, quat_fabric_after = view_fabric.get_world_poses() + torch.testing.assert_close(pos_fabric_after, new_positions, atol=1e-4, rtol=0) + torch.testing.assert_close(quat_fabric_after, new_orientations, atol=1e-4, rtol=0) diff --git a/source/isaaclab/test/terrains/check_height_field_subterrains.py b/source/isaaclab/test/terrains/check_height_field_subterrains.py index a8ec8a5377d2..972d4dc22884 100644 --- a/source/isaaclab/test/terrains/check_height_field_subterrains.py +++ b/source/isaaclab/test/terrains/check_height_field_subterrains.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +22,7 @@ """Rest everything follows.""" import os + import trimesh import isaaclab.terrains.height_field as hf_gen diff --git a/source/isaaclab/test/terrains/check_mesh_subterrains.py b/source/isaaclab/test/terrains/check_mesh_subterrains.py index 6a05341d1fd8..593b00e8fa23 100644 --- a/source/isaaclab/test/terrains/check_mesh_subterrains.py +++ b/source/isaaclab/test/terrains/check_mesh_subterrains.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -23,6 +23,7 @@ import argparse import os + import trimesh import isaaclab.terrains.trimesh as mesh_gen diff --git a/source/isaaclab/test/terrains/check_terrain_importer.py b/source/isaaclab/test/terrains/check_terrain_importer.py index 2de8b457e323..d88ec65c86d0 100644 --- a/source/isaaclab/test/terrains/check_terrain_importer.py +++ b/source/isaaclab/test/terrains/check_terrain_importer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -64,20 +64,18 @@ import numpy as np -import isaacsim.core.utils.prims as prim_utils import omni.kit import omni.kit.commands from isaacsim.core.api.materials import PhysicsMaterial from isaacsim.core.api.materials.preview_surface import PreviewSurface from isaacsim.core.api.objects import DynamicSphere -from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.cloner import GridCloner from isaacsim.core.prims import RigidPrim, SingleGeometryPrim, SingleRigidPrim from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.viewports import set_camera_view import isaaclab.sim as sim_utils import isaaclab.terrains as terrain_gen +from isaaclab.sim import SimulationCfg, SimulationContext from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG from isaaclab.terrains.terrain_importer import TerrainImporter from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -89,18 +87,9 @@ def main(): """Generates a terrain from isaaclab.""" # Load kit helper - sim_params = { - "use_gpu": True, - "use_gpu_pipeline": True, - "use_flatcache": True, - "use_fabric": True, - "enable_scene_query_support": True, - } - sim = SimulationContext( - physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, sim_params=sim_params, backend="torch", device="cuda:0" - ) + sim = SimulationContext(SimulationCfg()) # Set main camera - set_camera_view([0.0, 30.0, 25.0], [0.0, 0.0, -2.5]) + sim.set_camera_view(eye=(0.0, 30.0, 25.0), target=(0.0, 0.0, -2.5)) # Parameters num_balls = 2048 @@ -109,7 +98,7 @@ def main(): cloner = GridCloner(spacing=2.0) cloner.define_base_env("/World/envs") # Everything under the namespace "/World/envs/env_0" will be cloned - prim_utils.define_prim("/World/envs/env_0") + sim_utils.define_prim("/World/envs/env_0") # Handler for terrains importing terrain_importer_cfg = terrain_gen.TerrainImporterCfg( @@ -136,7 +125,7 @@ def main(): else: # -- Ball geometry cube_prim_path = omni.kit.commands.execute("CreateMeshPrimCommand", prim_type="Sphere")[1] - prim_utils.move_prim(cube_prim_path, "/World/envs/env_0/ball") + sim_utils.move_prim(cube_prim_path, "/World/envs/env_0/ball") # -- Ball physics SingleRigidPrim( prim_path="/World/envs/env_0/ball", mass=0.5, scale=(0.5, 0.5, 0.5), translation=(0.0, 0.0, 0.5) diff --git a/source/isaaclab/test/terrains/test_terrain_generator.py b/source/isaaclab/test/terrains/test_terrain_generator.py index 46f029ab7c9f..804176458cb5 100644 --- a/source/isaaclab/test/terrains/test_terrain_generator.py +++ b/source/isaaclab/test/terrains/test_terrain_generator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,16 +12,16 @@ """Rest everything follows.""" -import numpy as np import os import shutil -import torch -import isaacsim.core.utils.torch as torch_utils +import numpy as np import pytest +import torch from isaaclab.terrains import FlatPatchSamplingCfg, TerrainGenerator, TerrainGeneratorCfg from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG +from isaaclab.utils.seed import configure_seed @pytest.fixture @@ -65,7 +65,7 @@ def test_generation_reproducibility(use_global_seed, seed): Setting only locally is not tested as it is not supported. """ # set initial seed - torch_utils.set_seed(seed) + configure_seed(seed) # create terrain generator cfg = ROUGH_TERRAINS_CFG @@ -77,7 +77,7 @@ def test_generation_reproducibility(use_global_seed, seed): terrain_mesh_1 = terrain_generator.terrain_mesh.copy() # set seed again - torch_utils.set_seed(seed) + configure_seed(seed) # create terrain generator terrain_generator = TerrainGenerator(cfg=cfg) @@ -116,7 +116,7 @@ def test_generation_cache(output_dir, curriculum): # set a random seed to disturb the process # this is to ensure that the seed inside the terrain generator makes deterministic results - torch_utils.set_seed(12456) + configure_seed(12456) # create terrain generator with cache enabled terrain_generator = TerrainGenerator(cfg=cfg) diff --git a/source/isaaclab/test/terrains/test_terrain_importer.py b/source/isaaclab/test/terrains/test_terrain_importer.py index 26bacac387c2..05ed76e0811e 100644 --- a/source/isaaclab/test/terrains/test_terrain_importer.py +++ b/source/isaaclab/test/terrains/test_terrain_importer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,22 +12,23 @@ """Rest everything follows.""" +from typing import Literal + import numpy as np +import pytest import torch import trimesh -from typing import Literal -import isaacsim.core.utils.prims as prim_utils import omni.kit import omni.kit.commands -import pytest from isaacsim.core.api.materials import PhysicsMaterial, PreviewSurface from isaacsim.core.api.objects import DynamicSphere from isaacsim.core.cloner import GridCloner from isaacsim.core.prims import RigidPrim, SingleGeometryPrim, SingleRigidPrim from isaacsim.core.utils.extensions import enable_extension -from pxr import UsdGeom +from pxr import Usd, UsdGeom +import isaaclab.sim as sim_utils import isaaclab.terrains as terrain_gen from isaaclab.sim import PreviewSurfaceCfg, SimulationContext, build_simulation_context, get_first_matching_child_prim from isaaclab.terrains import TerrainImporter, TerrainImporterCfg @@ -55,7 +56,7 @@ def test_grid_clone_env_origins(device, env_spacing, num_envs): terrain_importer_origins = terrain_importer.env_origins # obtain env origins using grid cloner - grid_cloner_origins = _obtain_grid_cloner_env_origins(num_envs, env_spacing, device=sim.device) + grid_cloner_origins = _obtain_grid_cloner_env_origins(num_envs, env_spacing, stage=sim.stage, device=sim.device) # check if the env origins are the same torch.testing.assert_close(terrain_importer_origins, grid_cloner_origins, rtol=1e-5, atol=1e-5) @@ -242,13 +243,14 @@ def _obtain_collision_mesh(mesh_prim_path: str, mesh_type: Literal["Mesh", "Plan return None -def _obtain_grid_cloner_env_origins(num_envs: int, env_spacing: float, device: str) -> torch.Tensor: +def _obtain_grid_cloner_env_origins(num_envs: int, env_spacing: float, stage: Usd.Stage, device: str) -> torch.Tensor: """Obtain the env origins generated by IsaacSim GridCloner (grid_cloner.py).""" # create grid cloner cloner = GridCloner(spacing=env_spacing) cloner.define_base_env("/World/envs") envs_prim_paths = cloner.generate_paths("/World/envs/env", num_paths=num_envs) - prim_utils.define_prim("/World/envs/env_0") + # create source prim + stage.DefinePrim("/World/envs/env_0", "Xform") # clone envs using grid cloner env_origins = cloner.clone(source_prim_path="/World/envs/env_0", prim_paths=envs_prim_paths, replicate_physics=True) # return as tensor @@ -275,7 +277,7 @@ def _populate_scene(sim: SimulationContext, num_balls: int = 2048, geom_sphere: cloner = GridCloner(spacing=2.0) cloner.define_base_env("/World/envs") # Everything under the namespace "/World/envs/env_0" will be cloned - prim_utils.define_prim(prim_path="/World/envs/env_0", prim_type="Xform") + sim.stage.DefinePrim("/World/envs/env_0", "Xform") # Define the scene # -- Ball @@ -288,7 +290,7 @@ def _populate_scene(sim: SimulationContext, num_balls: int = 2048, geom_sphere: # -- Ball geometry enable_extension("omni.kit.primitive.mesh") cube_prim_path = omni.kit.commands.execute("CreateMeshPrimCommand", prim_type="Sphere")[1] - prim_utils.move_prim(cube_prim_path, "/World/envs/env_0/ball") + sim_utils.move_prim(cube_prim_path, "/World/envs/env_0/ball") # -- Ball physics SingleRigidPrim( prim_path="/World/envs/env_0/ball", mass=0.5, scale=(0.5, 0.5, 0.5), translation=(0.0, 0.0, 0.5) diff --git a/source/isaaclab/test/utils/test_assets.py b/source/isaaclab/test/utils/test_assets.py index fefb44f46c94..483c7d93d9fe 100644 --- a/source/isaaclab/test/utils/test_assets.py +++ b/source/isaaclab/test/utils/test_assets.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/utils/test_circular_buffer.py b/source/isaaclab/test/utils/test_circular_buffer.py index 6c66b00204cd..52a2c16829d8 100644 --- a/source/isaaclab/test/utils/test_circular_buffer.py +++ b/source/isaaclab/test/utils/test_circular_buffer.py @@ -1,11 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 torch - import pytest +import torch """Launch Isaac Sim Simulator first.""" diff --git a/source/isaaclab/test/utils/test_configclass.py b/source/isaaclab/test/utils/test_configclass.py index 6fbfb4ee8f90..0c024be03f3b 100644 --- a/source/isaaclab/test/utils/test_configclass.py +++ b/source/isaaclab/test/utils/test_configclass.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,13 +18,13 @@ import copy import os -import torch from collections.abc import Callable from dataclasses import MISSING, asdict, field from functools import wraps from typing import Any, ClassVar import pytest +import torch from isaaclab.utils.configclass import configclass from isaaclab.utils.dict import class_to_dict, dict_to_md5_hash, update_class_from_dict @@ -791,9 +791,9 @@ def test_functions_config(): """Tests having functions as values in the configuration instance.""" cfg = FunctionsDemoCfg() # check types - assert cfg.__annotations__["func"] == type(dummy_function1) - assert cfg.__annotations__["wrapped_func"] == type(wrapped_dummy_function3) - assert cfg.__annotations__["func_in_dict"] == dict + assert cfg.__annotations__["func"] is type(dummy_function1) + assert cfg.__annotations__["wrapped_func"] is type(wrapped_dummy_function3) + assert cfg.__annotations__["func_in_dict"] is dict # check calling assert cfg.func() == 1 assert cfg.wrapped_func() == 4 @@ -993,10 +993,10 @@ def test_config_with_class_type(): # since python 3.10, annotations are stored as strings annotations = {k: eval(v) if isinstance(v, str) else v for k, v in cfg.__annotations__.items()} # check types - assert annotations["class_name_1"] == type + assert annotations["class_name_1"] is type assert annotations["class_name_2"] == type[DummyClass] assert annotations["class_name_3"] == type[DummyClass] - assert annotations["class_name_4"] == ClassVar[type[DummyClass]] + assert annotations["class_name_4"] is ClassVar[type[DummyClass]] # check values assert cfg.class_name_1 == DummyClass assert cfg.class_name_2 == DummyClass diff --git a/source/isaaclab/test/utils/test_delay_buffer.py b/source/isaaclab/test/utils/test_delay_buffer.py index 40f31db341ed..a66802e72978 100644 --- a/source/isaaclab/test/utils/test_delay_buffer.py +++ b/source/isaaclab/test/utils/test_delay_buffer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ """Rest everything follows from here.""" -import torch from collections.abc import Generator import pytest +import torch from isaaclab.utils import DelayBuffer diff --git a/source/isaaclab/test/utils/test_dict.py b/source/isaaclab/test/utils/test_dict.py index 9713f8c1352c..35ce35f26577 100644 --- a/source/isaaclab/test/utils/test_dict.py +++ b/source/isaaclab/test/utils/test_dict.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/utils/test_episode_data.py b/source/isaaclab/test/utils/test_episode_data.py index 27f5db7bed30..e7d14adc8aa6 100644 --- a/source/isaaclab/test/utils/test_episode_data.py +++ b/source/isaaclab/test/utils/test_episode_data.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,8 @@ """Rest everything follows from here.""" -import torch - import pytest +import torch from isaaclab.utils.datasets import EpisodeData diff --git a/source/isaaclab/test/utils/test_hdf5_dataset_file_handler.py b/source/isaaclab/test/utils/test_hdf5_dataset_file_handler.py index 362958ae9b58..123ee95a1157 100644 --- a/source/isaaclab/test/utils/test_hdf5_dataset_file_handler.py +++ b/source/isaaclab/test/utils/test_hdf5_dataset_file_handler.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,10 +14,10 @@ import os import shutil import tempfile -import torch import uuid import pytest +import torch from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler diff --git a/source/isaaclab/test/utils/test_logger.py b/source/isaaclab/test/utils/test_logger.py new file mode 100644 index 000000000000..69df76f4c660 --- /dev/null +++ b/source/isaaclab/test/utils/test_logger.py @@ -0,0 +1,725 @@ +# 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 + +"""Tests for logging utilities.""" + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import logging +import os +import re +import tempfile +import time + +import pytest + +from isaaclab.utils.logger import ColoredFormatter, RateLimitFilter, configure_logging + + +# Fixtures +@pytest.fixture +def formatter(): + """Fixture providing a ColoredFormatter instance.""" + return ColoredFormatter("%(levelname)s: %(message)s") + + +@pytest.fixture +def test_message(): + """Fixture providing a test message string.""" + return "Test message" + + +@pytest.fixture +def rate_limit_filter(): + """Fixture providing a RateLimitFilter instance with 2 second interval.""" + return RateLimitFilter(interval_seconds=2) + + +""" +Tests for the ColoredFormatter class. +""" + + +def test_info_formatting(formatter, test_message): + """Test INFO level message formatting.""" + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg=test_message, + args=(), + exc_info=None, + ) + formatted = formatter.format(record) + + # INFO should use reset color (no color) + assert "\033[0m" in formatted + assert test_message in formatted + assert "INFO" in formatted + + +def test_debug_formatting(formatter, test_message): + """Test DEBUG level message formatting.""" + record = logging.LogRecord( + name="test", + level=logging.DEBUG, + pathname="test.py", + lineno=1, + msg=test_message, + args=(), + exc_info=None, + ) + formatted = formatter.format(record) + + # DEBUG should use reset color (no color) + assert "\033[0m" in formatted + assert test_message in formatted + assert "DEBUG" in formatted + + +def test_warning_formatting(formatter, test_message): + """Test WARNING level message formatting.""" + record = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg=test_message, + args=(), + exc_info=None, + ) + formatted = formatter.format(record) + + # WARNING should use yellow/orange color + assert "\033[33m" in formatted + assert test_message in formatted + assert "WARNING" in formatted + # Should end with reset + assert formatted.endswith("\033[0m") + + +def test_error_formatting(formatter, test_message): + """Test ERROR level message formatting.""" + record = logging.LogRecord( + name="test", + level=logging.ERROR, + pathname="test.py", + lineno=1, + msg=test_message, + args=(), + exc_info=None, + ) + formatted = formatter.format(record) + + # ERROR should use red color + assert "\033[31m" in formatted + assert test_message in formatted + assert "ERROR" in formatted + # Should end with reset + assert formatted.endswith("\033[0m") + + +def test_critical_formatting(formatter, test_message): + """Test CRITICAL level message formatting.""" + record = logging.LogRecord( + name="test", + level=logging.CRITICAL, + pathname="test.py", + lineno=1, + msg=test_message, + args=(), + exc_info=None, + ) + formatted = formatter.format(record) + + # CRITICAL should use bold red color + assert "\033[1;31m" in formatted + assert test_message in formatted + assert "CRITICAL" in formatted + # Should end with reset + assert formatted.endswith("\033[0m") + + +def test_color_codes_are_ansi(): + """Test that color codes are valid ANSI escape sequences.""" + # Test all defined colors + for level_name, color_code in ColoredFormatter.COLORS.items(): + # ANSI color codes should match pattern \033[m or \033[;m (for bold, etc.) + assert re.match(r"\033\[[\d;]+m", color_code), f"Invalid ANSI color code for {level_name}" + + # Test reset code + assert re.match(r"\033\[[\d;]+m", ColoredFormatter.RESET), "Invalid ANSI reset code" + + +def test_custom_format_string(test_message): + """Test that custom format strings work correctly.""" + custom_formatter = ColoredFormatter("%(name)s - %(levelname)s - %(message)s") + record = logging.LogRecord( + name="custom.logger", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg=test_message, + args=(), + exc_info=None, + ) + formatted = custom_formatter.format(record) + + assert "custom.logger" in formatted + assert "WARNING" in formatted + assert test_message in formatted + assert "\033[33m" in formatted # Warning color + + +""" +Tests for the RateLimitFilter class. +""" + + +def test_non_warning_messages_pass_through(rate_limit_filter): + """Test that non-WARNING messages always pass through the filter.""" + # Test INFO + info_record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Info message", + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(info_record) is True + + # Test ERROR + error_record = logging.LogRecord( + name="test", + level=logging.ERROR, + pathname="test.py", + lineno=1, + msg="Error message", + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(error_record) is True + + # Test DEBUG + debug_record = logging.LogRecord( + name="test", + level=logging.DEBUG, + pathname="test.py", + lineno=1, + msg="Debug message", + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(debug_record) is True + + +def test_first_warning_passes(rate_limit_filter): + """Test that the first WARNING message passes through.""" + record = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg="First warning", + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(record) is True + + +def test_duplicate_warning_within_interval_blocked(rate_limit_filter): + """Test that duplicate WARNING messages within interval are blocked.""" + message = "Duplicate warning" + + # First warning should pass + record1 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg=message, + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(record1) is True + + # Immediate duplicate should be blocked + record2 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=2, + msg=message, + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(record2) is False + + +def test_warning_after_interval_passes(): + """Test that WARNING messages pass after the rate limit interval.""" + message = "Rate limited warning" + filter_short = RateLimitFilter(interval_seconds=1) + + # First warning should pass + record1 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg=message, + args=(), + exc_info=None, + ) + assert filter_short.filter(record1) is True + + # Immediate duplicate should be blocked + record2 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=2, + msg=message, + args=(), + exc_info=None, + ) + assert filter_short.filter(record2) is False + + # Wait for interval to pass + time.sleep(1.1) + + # After interval, same message should pass again + record3 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=3, + msg=message, + args=(), + exc_info=None, + ) + assert filter_short.filter(record3) is True + + +def test_different_warnings_not_rate_limited(rate_limit_filter): + """Test that different WARNING messages are not rate limited together.""" + # First warning + record1 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg="Warning A", + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(record1) is True + + # Different warning should also pass + record2 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=2, + msg="Warning B", + args=(), + exc_info=None, + ) + assert rate_limit_filter.filter(record2) is True + + +def test_custom_interval(): + """Test that custom interval seconds work correctly.""" + custom_filter = RateLimitFilter(interval_seconds=1) + assert custom_filter.interval == 1 + + long_filter = RateLimitFilter(interval_seconds=10) + assert long_filter.interval == 10 + + +def test_last_emitted_tracking(rate_limit_filter): + """Test that the filter correctly tracks last emission times.""" + message1 = "Message 1" + message2 = "Message 2" + + # Emit first message + record1 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg=message1, + args=(), + exc_info=None, + ) + rate_limit_filter.filter(record1) + + # Check that message1 is tracked + assert message1 in rate_limit_filter.last_emitted + + # Emit second message + record2 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=2, + msg=message2, + args=(), + exc_info=None, + ) + rate_limit_filter.filter(record2) + + # Check that both messages are tracked + assert message1 in rate_limit_filter.last_emitted + assert message2 in rate_limit_filter.last_emitted + + # Timestamps should be different (though very close) + assert rate_limit_filter.last_emitted[message1] <= rate_limit_filter.last_emitted[message2] + + +def test_formatted_message_warnings(rate_limit_filter): + """Test rate limiting with formatted WARNING messages.""" + # Test with string formatting + record1 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=1, + msg="Warning: value=%d", + args=(42,), + exc_info=None, + ) + assert rate_limit_filter.filter(record1) is True + + # Same formatted message should be blocked + record2 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=2, + msg="Warning: value=%d", + args=(42,), + exc_info=None, + ) + assert rate_limit_filter.filter(record2) is False + + # Different args create different message, should pass + record3 = logging.LogRecord( + name="test", + level=logging.WARNING, + pathname="test.py", + lineno=3, + msg="Warning: value=%d", + args=(99,), + exc_info=None, + ) + assert rate_limit_filter.filter(record3) is True + + +""" +Integration Tests. + +Tests that the filter and formatter work together in a logger. +""" + + +def test_filter_and_formatter_together(): + """Test that filter and formatter work together in a logger.""" + # Create a logger with both filter and formatter + test_logger = logging.getLogger("test_integration") + test_logger.setLevel(logging.DEBUG) + + # Remove any existing handlers + test_logger.handlers.clear() + + # Create handler with colored formatter + handler = logging.StreamHandler() + handler.setFormatter(ColoredFormatter("%(levelname)s: %(message)s")) + + # Add rate limit filter + rate_filter = RateLimitFilter(interval_seconds=1) + handler.addFilter(rate_filter) + + test_logger.addHandler(handler) + + # Test that logger is set up correctly + assert len(test_logger.handlers) == 1 + assert isinstance(test_logger.handlers[0].formatter, ColoredFormatter) + + # Clean up + test_logger.handlers.clear() + + +def test_default_initialization(): + """Test that classes can be initialized with default parameters.""" + # ColoredFormatter with default format + formatter = ColoredFormatter() + assert formatter is not None + + # RateLimitFilter with default interval + filter_obj = RateLimitFilter() + assert filter_obj.interval == 5 # default is 5 seconds + + +""" +Tests for the configure_logging function. +""" + + +def test_configure_logging_basic(): + """Test basic configure_logging functionality without file logging.""" + # Setup logger without file logging + logger = configure_logging(logging_level="INFO", save_logs_to_file=False) + + # Should return root logger + assert logger is not None + assert logger is logging.getLogger() + # Root logger is always set to DEBUG to ensure all messages are logged + assert logger.level == logging.DEBUG + + # Should have exactly one handler (stream handler) + assert len(logger.handlers) == 1 + + # Stream handler should have ColoredFormatter + stream_handler = logger.handlers[0] + assert isinstance(stream_handler, logging.StreamHandler) + assert isinstance(stream_handler.formatter, ColoredFormatter) + assert stream_handler.level == logging.INFO + + # Should have RateLimitFilter + assert len(stream_handler.filters) > 0 + rate_filter = stream_handler.filters[0] + assert isinstance(rate_filter, RateLimitFilter) + assert rate_filter.interval == 5 + + +def test_configure_logging_with_file(): + """Test configure_logging with file logging enabled.""" + # Setup logger with file logging + with tempfile.TemporaryDirectory() as temp_dir: + logger = configure_logging(logging_level="DEBUG", save_logs_to_file=True, log_dir=temp_dir) + + # Should return root logger + assert logger is not None + # Root logger is always set to DEBUG + assert logger.level == logging.DEBUG + + # Should have two handlers (stream + file) + assert len(logger.handlers) == 2 + + # Check stream handler + stream_handler = logger.handlers[0] + assert isinstance(stream_handler, logging.StreamHandler) + assert isinstance(stream_handler.formatter, ColoredFormatter) + assert stream_handler.level == logging.DEBUG + + # Check file handler + file_handler = logger.handlers[1] + assert isinstance(file_handler, logging.FileHandler) + assert file_handler.level == logging.DEBUG + + # Verify log file was created + log_files = [f for f in os.listdir(temp_dir) if f.startswith("isaaclab_")] + assert len(log_files) == 1 + + +def test_configure_logging_levels(): + """Test configure_logging with different logging levels.""" + from typing import Literal + + levels: list[Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]] = [ + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL", + ] + level_values = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + } + + for level_str in levels: + logger = configure_logging(logging_level=level_str, save_logs_to_file=False) + # Root logger is always set to DEBUG to ensure all messages are logged + assert logger.level == logging.DEBUG + # Handler level should match the requested level + assert logger.handlers[0].level == level_values[level_str] + + +def test_configure_logging_removes_existing_handlers(): + """Test that configure_logging removes existing handlers.""" + # Get root logger and add a dummy handler + root_logger = logging.getLogger() + dummy_handler = logging.StreamHandler() + root_logger.addHandler(dummy_handler) + + initial_handler_count = len(root_logger.handlers) + assert initial_handler_count > 0 + + # Setup logger should remove existing handlers + logger = configure_logging(logging_level="INFO", save_logs_to_file=False) + + # Should only have the new handler + assert len(logger.handlers) == 1 + assert dummy_handler not in logger.handlers + + +def test_configure_logging_default_log_dir(): + """Test configure_logging uses temp directory when log_dir is None.""" + + logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=None) + + # Root logger is always set to DEBUG + assert logger.level == logging.DEBUG + + # Should have file handler + assert len(logger.handlers) == 2 + file_handler = logger.handlers[1] + assert isinstance(file_handler, logging.FileHandler) + + # File should be in temp directory + log_file_path = file_handler.baseFilename + assert os.path.dirname(log_file_path) == os.path.join(tempfile.gettempdir(), "isaaclab", "logs") + assert os.path.basename(log_file_path).startswith("isaaclab_") + + # Cleanup + if os.path.exists(log_file_path): + os.remove(log_file_path) + + +def test_configure_logging_custom_log_dir(): + """Test configure_logging with custom log directory.""" + with tempfile.TemporaryDirectory() as temp_dir: + custom_log_dir = os.path.join(temp_dir, "custom_logs") + + logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=custom_log_dir) + + # Custom directory should be created + assert os.path.exists(custom_log_dir) + assert os.path.isdir(custom_log_dir) + + # Root logger is always set to DEBUG + assert logger.level == logging.DEBUG + + # Log file should be in custom directory + file_handler = logger.handlers[1] + assert isinstance(file_handler, logging.FileHandler) + log_file_path = file_handler.baseFilename + assert os.path.dirname(log_file_path) == custom_log_dir + + +def test_configure_logging_log_file_format(): + """Test that log file has correct timestamp format.""" + with tempfile.TemporaryDirectory() as temp_dir: + logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=temp_dir) + + # Root logger is always set to DEBUG + assert logger.level == logging.DEBUG + + # Get log file name + file_handler = logger.handlers[1] + assert isinstance(file_handler, logging.FileHandler) + log_file_path = file_handler.baseFilename + log_filename = os.path.basename(log_file_path) + + # Check filename format: isaaclab_YYYY-MM-DD_HH-MM-SS.log + pattern = r"isaaclab_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log" + assert re.match(pattern, log_filename), f"Log filename {log_filename} doesn't match expected pattern" + + +def test_configure_logging_file_formatter(): + """Test that file handler has more detailed formatter than stream handler.""" + with tempfile.TemporaryDirectory() as temp_dir: + logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=temp_dir) + + # Root logger is always set to DEBUG + assert logger.level == logging.DEBUG + + stream_handler = logger.handlers[0] + file_handler = logger.handlers[1] + + # Stream formatter should exist and be ColoredFormatter + assert stream_handler.formatter is not None + assert isinstance(stream_handler.formatter, ColoredFormatter) + stream_format = stream_handler.formatter._fmt # type: ignore + assert stream_format is not None + assert "%(asctime)s" in stream_format + assert "%(filename)s" in stream_format + + # File formatter should exist and include line numbers + assert file_handler.formatter is not None + assert isinstance(file_handler.formatter, logging.Formatter) + file_format = file_handler.formatter._fmt # type: ignore + assert file_format is not None + assert "%(asctime)s" in file_format + assert "%(lineno)d" in file_format + + # File handler should always use DEBUG level + assert file_handler.level == logging.DEBUG + + +def test_configure_logging_multiple_calls(): + """Test that multiple configure_logging calls properly cleanup.""" + # First setup + logger1 = configure_logging(logging_level="INFO", save_logs_to_file=False) + handler_count_1 = len(logger1.handlers) + + # Second setup should remove previous handlers + logger2 = configure_logging(logging_level="DEBUG", save_logs_to_file=False) + handler_count_2 = len(logger2.handlers) + + # Should be same logger (root logger) + assert logger1 is logger2 + + # Should have same number of handlers (old ones removed) + assert handler_count_1 == handler_count_2 == 1 + + +def test_configure_logging_actual_logging(): + """Test that logger actually logs messages correctly.""" + import io + + # Capture stdout + captured_output = io.StringIO() + + # Setup logger + logger = configure_logging(logging_level="INFO", save_logs_to_file=False) + + # Temporarily redirect handler to captured output + stream_handler = logger.handlers[0] + assert isinstance(stream_handler, logging.StreamHandler) + original_stream = stream_handler.stream # type: ignore + stream_handler.stream = captured_output # type: ignore + + # Log some messages + test_logger = logging.getLogger("test_module") + test_logger.info("Test info message") + test_logger.warning("Test warning message") + test_logger.debug("Test debug message") # Should not appear (level is INFO) + + # Restore original stream + stream_handler.stream = original_stream # type: ignore + + # Check output + output = captured_output.getvalue() + assert "Test info message" in output + assert "Test warning message" in output + assert "Test debug message" not in output # DEBUG < INFO + assert "INFO" in output + assert "WARNING" in output diff --git a/source/isaaclab/test/utils/test_math.py b/source/isaaclab/test/utils/test_math.py index bb436c909dab..2f256728e9ee 100644 --- a/source/isaaclab/test/utils/test_math.py +++ b/source/isaaclab/test/utils/test_math.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,13 +17,13 @@ """Rest everything follows.""" import math +from math import pi as PI + import numpy as np +import pytest import scipy.spatial.transform as scipy_tf import torch import torch.utils.benchmark as benchmark -from math import pi as PI - -import pytest import isaaclab.utils.math as math_utils @@ -108,7 +108,9 @@ def test_normalize(device, size): @pytest.mark.parametrize("device", ("cpu", "cuda:0")) def test_copysign(device): - """Test copysign by copying a sign from both a negative and positive value and verify that the new sign is the same.""" + """Test copysign by copying a sign from both a negative and positive value and + verify that the new sign is the same. + """ size = (10, 2) @@ -164,10 +166,12 @@ def test_axis_angle_from_quat(device): # Quaternions of the form (2,4) and (2,2,4) quats = [ torch.Tensor([[1.0, 0.0, 0.0, 0.0], [0.8418536, 0.142006, 0.0, 0.5206887]]).to(device), - torch.Tensor([ - [[1.0, 0.0, 0.0, 0.0], [0.8418536, 0.142006, 0.0, 0.5206887]], - [[1.0, 0.0, 0.0, 0.0], [0.9850375, 0.0995007, 0.0995007, 0.0995007]], - ]).to(device), + torch.Tensor( + [ + [[1.0, 0.0, 0.0, 0.0], [0.8418536, 0.142006, 0.0, 0.5206887]], + [[1.0, 0.0, 0.0, 0.0], [0.9850375, 0.0995007, 0.0995007, 0.0995007]], + ] + ).to(device), ] # Angles of the form (2,3) and (2,2,3) @@ -359,7 +363,10 @@ def test_convention_converter(device): @pytest.mark.parametrize("device", ("cpu", "cuda:0")) @pytest.mark.parametrize("size", ((10, 4), (5, 3, 4))) def test_convert_quat(device, size): - """Test convert_quat from xyzw to wxyz and back to xyzw and verify the correct rolling of the tensor. Also check the correct exceptions are raised for bad inputs for the quaternion and the 'to'.""" + """Test convert_quat from "xyzw" to "wxyz" and back to "xyzw" and verify the correct rolling of the tensor. + + Also check the correct exceptions are raised for bad inputs for the quaternion and the 'to'. + """ quat = torch.zeros(size, device=device) quat[..., 0] = 1.0 @@ -598,10 +605,12 @@ def test_pose_inv(): np.testing.assert_array_almost_equal(result, expected, decimal=DECIMAL_PRECISION) # Check against a batch of matrices - test_mats = torch.stack([ - math_utils.generate_random_transformation_matrix(pos_boundary=10, rot_boundary=(2 * math.pi)) - for _ in range(100) - ]) + test_mats = torch.stack( + [ + math_utils.generate_random_transformation_matrix(pos_boundary=10, rot_boundary=(2 * math.pi)) + for _ in range(100) + ] + ) result = np.array(math_utils.pose_inv(test_mats)) expected = np.linalg.inv(np.array(test_mats)) np.testing.assert_array_almost_equal(result, expected, decimal=DECIMAL_PRECISION) @@ -691,7 +700,9 @@ def test_quat_box_minus_and_quat_box_plus(device): @pytest.mark.parametrize("t12_inputs", ["True", "False"]) @pytest.mark.parametrize("q12_inputs", ["True", "False"]) def test_combine_frame_transforms(device, t12_inputs, q12_inputs): - """Test combine_frame_transforms such that inputs for delta translation and delta rotation can be None or specified.""" + """Test combine_frame_transforms such that inputs for delta translation and delta rotation + can be :obj:`None` or specified. + """ n = 1024 t01 = torch.zeros((n, 3), device=device) t01.uniform_(-1000.0, 1000.0) @@ -728,7 +739,11 @@ def test_combine_frame_transforms(device, t12_inputs, q12_inputs): @pytest.mark.parametrize("t02_inputs", ["True", "False"]) @pytest.mark.parametrize("q02_inputs", ["True", "False"]) def test_subtract_frame_transforms(device, t02_inputs, q02_inputs): - """Test subtract_frame_transforms with specified and unspecified inputs for t02 and q02. Verify that it is the inverse operation to combine_frame_transforms.""" + """Test subtract_frame_transforms with specified and unspecified inputs for t02 and q02. + + This test verifies that :meth:`~isaaclab.utils.math_utils.subtract_frame_transforms` is the inverse operation + to :meth:`~isaaclab.utils.math_utils.combine_frame_transforms`. + .""" n = 1024 t01 = torch.zeros((n, 3), device=device) t01.uniform_(-1000.0, 1000.0) @@ -1249,36 +1264,48 @@ def test_euler_xyz_from_quat(): """ quats = [ torch.Tensor([[1.0, 0.0, 0.0, 0.0]]), # 0° around x, y, z - torch.Tensor([ - [0.9238795, 0.3826834, 0.0, 0.0], # 45° around x - [0.9238795, 0.0, -0.3826834, 0.0], # -45° around y - [0.9238795, 0.0, 0.0, -0.3826834], # -45° around z - ]), - torch.Tensor([ - [0.7071068, -0.7071068, 0.0, 0.0], # -90° around x - [0.7071068, 0.0, 0.0, -0.7071068], # -90° around z - ]), - torch.Tensor([ - [0.3826834, -0.9238795, 0.0, 0.0], # -135° around x - [0.3826834, 0.0, 0.0, -0.9238795], # -135° around y - ]), + torch.Tensor( + [ + [0.9238795, 0.3826834, 0.0, 0.0], # 45° around x + [0.9238795, 0.0, -0.3826834, 0.0], # -45° around y + [0.9238795, 0.0, 0.0, -0.3826834], # -45° around z + ] + ), + torch.Tensor( + [ + [0.7071068, -0.7071068, 0.0, 0.0], # -90° around x + [0.7071068, 0.0, 0.0, -0.7071068], # -90° around z + ] + ), + torch.Tensor( + [ + [0.3826834, -0.9238795, 0.0, 0.0], # -135° around x + [0.3826834, 0.0, 0.0, -0.9238795], # -135° around y + ] + ), ] expected_euler_angles = [ torch.Tensor([[0.0, 0.0, 0.0]]), # identity - torch.Tensor([ - [torch.pi / 4, 0.0, 0.0], # 45° about x - [0.0, -torch.pi / 4, 0.0], # -45° about y - [0.0, 0.0, -torch.pi / 4], # -45° about z - ]), - torch.Tensor([ - [-torch.pi / 2, 0.0, 0.0], # -90° about x - [0.0, 0.0, -torch.pi / 2], # -90° about z - ]), - torch.Tensor([ - [-3 * torch.pi / 4, 0.0, 0.0], # -135° about x - [0.0, 0.0, -3 * torch.pi / 4], # -135° about y - ]), + torch.Tensor( + [ + [torch.pi / 4, 0.0, 0.0], # 45° about x + [0.0, -torch.pi / 4, 0.0], # -45° about y + [0.0, 0.0, -torch.pi / 4], # -45° about z + ] + ), + torch.Tensor( + [ + [-torch.pi / 2, 0.0, 0.0], # -90° about x + [0.0, 0.0, -torch.pi / 2], # -90° about z + ] + ), + torch.Tensor( + [ + [-3 * torch.pi / 4, 0.0, 0.0], # -135° about x + [0.0, 0.0, -3 * torch.pi / 4], # -135° about y + ] + ), ] # Test 1: default no-wrap range from (-π, π] diff --git a/source/isaaclab/test/utils/test_modifiers.py b/source/isaaclab/test/utils/test_modifiers.py index 537c56d1f623..9cdd9a5d6631 100644 --- a/source/isaaclab/test/utils/test_modifiers.py +++ b/source/isaaclab/test/utils/test_modifiers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ """Rest everything follows.""" -import torch from dataclasses import MISSING import pytest +import torch import isaaclab.utils.modifiers as modifiers from isaaclab.utils import configclass diff --git a/source/isaaclab/test/utils/test_noise.py b/source/isaaclab/test/utils/test_noise.py index 7a25951e6f54..176371d381f6 100644 --- a/source/isaaclab/test/utils/test_noise.py +++ b/source/isaaclab/test/utils/test_noise.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +12,8 @@ """Rest everything follows.""" -import torch - import pytest +import torch import isaaclab.utils.noise as noise diff --git a/source/isaaclab/test/utils/test_string.py b/source/isaaclab/test/utils/test_string.py index f697509586b2..d171a3885e10 100644 --- a/source/isaaclab/test/utils/test_string.py +++ b/source/isaaclab/test/utils/test_string.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/utils/test_timer.py b/source/isaaclab/test/utils/test_timer.py index 4b866a90c105..8d99db3b2d80 100644 --- a/source/isaaclab/test/utils/test_timer.py +++ b/source/isaaclab/test/utils/test_timer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab/test/utils/test_version.py b/source/isaaclab/test/utils/test_version.py new file mode 100644 index 000000000000..ba737b53643e --- /dev/null +++ b/source/isaaclab/test/utils/test_version.py @@ -0,0 +1,152 @@ +# 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 + +"""Tests for version comparison utilities.""" + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import pytest +from packaging.version import Version + +from isaaclab.utils.version import compare_versions, get_isaac_sim_version + + +def test_get_isaac_sim_version(): + """Test that get_isaac_sim_version returns cached Version object.""" + # Call twice to ensure caching works + version1 = get_isaac_sim_version() + version2 = get_isaac_sim_version() + + # Should return the same object (cached) + assert version1 is version2 + + # Should return a packaging.version.Version object + assert isinstance(version1, Version) + + # Major version should be reasonable + assert version1.major >= 4 + + # Minor and micro should be non-negative + assert version1.minor >= 0 + assert version1.micro >= 0 + + +def test_get_isaac_sim_version_format(): + """Test that get_isaac_sim_version returns correct format.""" + isaac_version = get_isaac_sim_version() + + # Should be able to convert to string + version_str = str(isaac_version) + assert isinstance(version_str, str) + + # Should have proper format (e.g., "5.0.0") + parts = version_str.split(".") + assert len(parts) >= 3 + + # Can access components + assert hasattr(isaac_version, "major") + assert hasattr(isaac_version, "minor") + assert hasattr(isaac_version, "micro") + + +def test_version_caching_performance(): + """Test that caching improves performance for version checks.""" + # First call (will cache) + version1 = get_isaac_sim_version() + + # Subsequent calls should be instant (from cache) + for _ in range(100): + version = get_isaac_sim_version() + assert version == version1 + assert version is version1 # Should be the exact same object + + +def test_version_comparison_operators(): + """Test that Version objects support natural comparisons.""" + isaac_version = get_isaac_sim_version() + + # Should support comparison operators + assert isaac_version >= Version("4.0.0") + assert isaac_version == isaac_version + + # Test less than + if isaac_version.major >= 5: + assert isaac_version > Version("4.5.0") + assert isaac_version >= Version("5.0.0") + + # Test not equal + assert isaac_version != Version("0.0.1") + + +@pytest.mark.parametrize( + "v1,v2,expected", + [ + # Equal versions + ("1.0.0", "1.0.0", 0), + ("2.5.3", "2.5.3", 0), + # Equal with different lengths (implicit zeros) + ("1.0", "1.0.0", 0), + ("1", "1.0.0.0", 0), + ("2.5", "2.5.0.0", 0), + # Major version differences + ("2.0.0", "1.0.0", 1), + ("1.0.0", "2.0.0", -1), + ("2.0.0", "1.99.99", 1), + # Minor version differences + ("1.5.0", "1.4.0", 1), + ("1.4.0", "1.5.0", -1), + ("1.10.0", "1.9.99", 1), + # Patch version differences + ("1.0.5", "1.0.4", 1), + ("1.0.4", "1.0.5", -1), + ("2.5.10", "2.5.9", 1), + # Single/double digit versions + ("2", "1", 1), + ("1", "2", -1), + ("1.5", "1.4", 1), + # Extended versions + ("1.0.0.1", "1.0.0.0", 1), + ("1.2.3.4.5", "1.2.3.4", 1), + # Zero versions + ("0.0.1", "0.0.0", 1), + ("0.1.0", "0.0.9", 1), + ("0", "0.0.0", 0), + # Large numbers + ("100.200.300", "100.200.299", 1), + ("999.999.999", "1000.0.0", -1), + ], +) +def test_version_comparisons(v1, v2, expected): + """Test version comparisons with various scenarios.""" + assert compare_versions(v1, v2) == expected + + +def test_symmetry(): + """Test anti-symmetric property: if v1 < v2, then v2 > v1.""" + test_pairs = [("1.0.0", "2.0.0"), ("1.5.3", "1.4.9"), ("1.0.0", "1.0.0")] + + for v1, v2 in test_pairs: + result1 = compare_versions(v1, v2) + result2 = compare_versions(v2, v1) + + if result1 == 0: + assert result2 == 0 + else: + assert result1 == -result2 + + +def test_transitivity(): + """Test transitive property: if v1 < v2 < v3, then v1 < v3.""" + v1, v2, v3 = "1.0.0", "2.0.0", "3.0.0" + assert compare_versions(v1, v2) == -1 + assert compare_versions(v2, v3) == -1 + assert compare_versions(v1, v3) == -1 diff --git a/source/isaaclab/test/utils/test_wrench_composer.py b/source/isaaclab/test/utils/test_wrench_composer.py new file mode 100644 index 000000000000..3cc88b3b9028 --- /dev/null +++ b/source/isaaclab/test/utils/test_wrench_composer.py @@ -0,0 +1,712 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +import numpy as np +import pytest +import torch +import warp as wp + +from isaaclab.assets import RigidObject +from isaaclab.utils.wrench_composer import WrenchComposer + + +class MockAssetData: + """Mock data class that provides body link positions and quaternions.""" + + def __init__( + self, + num_envs: int, + num_bodies: int, + device: str, + link_pos: torch.Tensor | None = None, + link_quat: torch.Tensor | None = None, + ): + """Initialize mock asset data. + + Args: + num_envs: Number of environments. + num_bodies: Number of bodies. + device: Device to use. + link_pos: Optional link positions (num_envs, num_bodies, 3). Defaults to zeros. + link_quat: Optional link quaternions in (w, x, y, z) format (num_envs, num_bodies, 4). + Defaults to identity quaternion. + """ + if link_pos is not None: + self.body_link_pos_w = link_pos.to(device=device, dtype=torch.float32) + else: + self.body_link_pos_w = torch.zeros((num_envs, num_bodies, 3), dtype=torch.float32, device=device) + + if link_quat is not None: + self.body_link_quat_w = link_quat.to(device=device, dtype=torch.float32) + else: + # Identity quaternion (w, x, y, z) = (1, 0, 0, 0) + self.body_link_quat_w = torch.zeros((num_envs, num_bodies, 4), dtype=torch.float32, device=device) + self.body_link_quat_w[..., 0] = 1.0 + + +class MockRigidObject: + """Mock RigidObject that provides the minimal interface required by WrenchComposer. + + This mock enables testing WrenchComposer in isolation without requiring a full simulation setup. + It passes isinstance checks by registering as a virtual subclass of RigidObject. + """ + + def __init__( + self, + num_envs: int, + num_bodies: int, + device: str, + link_pos: torch.Tensor | None = None, + link_quat: torch.Tensor | None = None, + ): + """Initialize mock rigid object. + + Args: + num_envs: Number of environments. + num_bodies: Number of bodies. + device: Device to use. + link_pos: Optional link positions (num_envs, num_bodies, 3). + link_quat: Optional link quaternions in (w, x, y, z) format (num_envs, num_bodies, 4). + """ + self.num_instances = num_envs + self.num_bodies = num_bodies + self.device = device + self.data = MockAssetData(num_envs, num_bodies, device, link_pos, link_quat) + + +# --- Helper functions for quaternion math --- + + +def quat_rotate_inv_np(quat_wxyz: np.ndarray, vec: np.ndarray) -> np.ndarray: + """Rotate a vector by the inverse of a quaternion (numpy). + + Args: + quat_wxyz: Quaternion in (w, x, y, z) format. Shape: (..., 4) + vec: Vector to rotate. Shape: (..., 3) + + Returns: + Rotated vector. Shape: (..., 3) + """ + # Extract components + w = quat_wxyz[..., 0:1] + xyz = quat_wxyz[..., 1:4] + + # For inverse rotation, we conjugate the quaternion (negate xyz) + # q^-1 * v * q = q_conj * v * q_conj^-1 for unit quaternion + # Using the formula: v' = v + 2*w*(xyz x v) + 2*(xyz x (xyz x v)) + # But for inverse: use -xyz + + # Cross product: xyz x vec + t = 2.0 * np.cross(-xyz, vec, axis=-1) + # Result: vec + w*t + xyz x t + return vec + w * t + np.cross(-xyz, t, axis=-1) + + +def random_unit_quaternion_np(rng: np.random.Generator, shape: tuple) -> np.ndarray: + """Generate random unit quaternions in (w, x, y, z) format. + + Args: + rng: Random number generator. + shape: Output shape, e.g. (num_envs, num_bodies). + + Returns: + Random unit quaternions. Shape: (*shape, 4) + """ + # Generate random quaternion components + q = rng.standard_normal(shape + (4,)).astype(np.float32) + # Normalize to unit quaternion + q = q / np.linalg.norm(q, axis=-1, keepdims=True) + return q + + +# Register MockRigidObject as a virtual subclass of RigidObject +# This allows isinstance(mock, RigidObject) to return True +RigidObject.register(MockRigidObject) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100, 1000]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5, 10]) +def test_wrench_composer_add_force(device: str, num_envs: int, num_bodies: int): + # Initialize random number generator + rng = np.random.default_rng(seed=0) + + for _ in range(10): + mock_asset = MockRigidObject(num_envs, num_bodies, device) + wrench_composer = WrenchComposer(mock_asset) + # Initialize hand-calculated composed force + hand_calculated_composed_force_np = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + for _ in range(10): + # Get random number of envs and bodies and their indices + num_envs_np = rng.integers(1, num_envs, endpoint=True) + num_bodies_np = rng.integers(1, num_bodies, endpoint=True) + env_ids_np = rng.choice(num_envs, size=num_envs_np, replace=False) + body_ids_np = rng.choice(num_bodies, size=num_bodies_np, replace=False) + # Convert to warp arrays + env_ids = wp.from_numpy(env_ids_np, dtype=wp.int32, device=device) + body_ids = wp.from_numpy(body_ids_np, dtype=wp.int32, device=device) + # Get random forces + forces_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + forces = wp.from_numpy(forces_np, dtype=wp.vec3f, device=device) + # Add forces to wrench composer + wrench_composer.add_forces_and_torques(forces=forces, body_ids=body_ids, env_ids=env_ids) + # Add forces to hand-calculated composed force + hand_calculated_composed_force_np[env_ids_np[:, None], body_ids_np[None, :], :] += forces_np + # Get composed force from wrench composer + composed_force_np = wrench_composer.composed_force.numpy() + assert np.allclose(composed_force_np, hand_calculated_composed_force_np, atol=1, rtol=1e-7) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100, 1000]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5, 10]) +def test_wrench_composer_add_torque(device: str, num_envs: int, num_bodies: int): + # Initialize random number generator + rng = np.random.default_rng(seed=1) + + for _ in range(10): + mock_asset = MockRigidObject(num_envs, num_bodies, device) + wrench_composer = WrenchComposer(mock_asset) + # Initialize hand-calculated composed torque + hand_calculated_composed_torque_np = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + for _ in range(10): + # Get random number of envs and bodies and their indices + num_envs_np = rng.integers(1, num_envs, endpoint=True) + num_bodies_np = rng.integers(1, num_bodies, endpoint=True) + env_ids_np = rng.choice(num_envs, size=num_envs_np, replace=False) + body_ids_np = rng.choice(num_bodies, size=num_bodies_np, replace=False) + # Convert to warp arrays + env_ids = wp.from_numpy(env_ids_np, dtype=wp.int32, device=device) + body_ids = wp.from_numpy(body_ids_np, dtype=wp.int32, device=device) + # Get random torques + torques_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + torques = wp.from_numpy(torques_np, dtype=wp.vec3f, device=device) + # Add torques to wrench composer + wrench_composer.add_forces_and_torques(torques=torques, body_ids=body_ids, env_ids=env_ids) + # Add torques to hand-calculated composed torque + hand_calculated_composed_torque_np[env_ids_np[:, None], body_ids_np[None, :], :] += torques_np + # Get composed torque from wrench composer + composed_torque_np = wrench_composer.composed_torque.numpy() + assert np.allclose(composed_torque_np, hand_calculated_composed_torque_np, atol=1, rtol=1e-7) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100, 1000]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5, 10]) +def test_add_forces_at_positons(device: str, num_envs: int, num_bodies: int): + """Test adding forces at local positions (offset from link frame).""" + rng = np.random.default_rng(seed=2) + + for _ in range(10): + # Initialize wrench composer + mock_asset = MockRigidObject(num_envs, num_bodies, device) + wrench_composer = WrenchComposer(mock_asset) + # Initialize hand-calculated composed force + hand_calculated_composed_force_np = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + # Initialize hand-calculated composed torque + hand_calculated_composed_torque_np = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + for _ in range(10): + # Get random number of envs and bodies and their indices + num_envs_np = rng.integers(1, num_envs, endpoint=True) + num_bodies_np = rng.integers(1, num_bodies, endpoint=True) + env_ids_np = rng.choice(num_envs, size=num_envs_np, replace=False) + body_ids_np = rng.choice(num_bodies, size=num_bodies_np, replace=False) + # Convert to warp arrays + env_ids = wp.from_numpy(env_ids_np, dtype=wp.int32, device=device) + body_ids = wp.from_numpy(body_ids_np, dtype=wp.int32, device=device) + # Get random forces + forces_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + positions_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + forces = wp.from_numpy(forces_np, dtype=wp.vec3f, device=device) + positions = wp.from_numpy(positions_np, dtype=wp.vec3f, device=device) + # Add forces at positions to wrench composer + wrench_composer.add_forces_and_torques( + forces=forces, positions=positions, body_ids=body_ids, env_ids=env_ids + ) + # Add forces to hand-calculated composed force + hand_calculated_composed_force_np[env_ids_np[:, None], body_ids_np[None, :], :] += forces_np + # Add torques to hand-calculated composed torque: torque = cross(position, force) + torques_from_forces = np.cross(positions_np, forces_np) + for i in range(num_envs_np): + for j in range(num_bodies_np): + hand_calculated_composed_torque_np[env_ids_np[i], body_ids_np[j], :] += torques_from_forces[i, j, :] + + # Get composed force from wrench composer + composed_force_np = wrench_composer.composed_force.numpy() + assert np.allclose(composed_force_np, hand_calculated_composed_force_np, atol=1, rtol=1e-7) + # Get composed torque from wrench composer + composed_torque_np = wrench_composer.composed_torque.numpy() + assert np.allclose(composed_torque_np, hand_calculated_composed_torque_np, atol=1, rtol=1e-7) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100, 1000]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5, 10]) +def test_add_torques_at_position(device: str, num_envs: int, num_bodies: int): + rng = np.random.default_rng(seed=3) + + for _ in range(10): + mock_asset = MockRigidObject(num_envs, num_bodies, device) + wrench_composer = WrenchComposer(mock_asset) + # Initialize hand-calculated composed torque + hand_calculated_composed_torque_np = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + for _ in range(10): + # Get random number of envs and bodies and their indices + num_envs_np = rng.integers(1, num_envs, endpoint=True) + num_bodies_np = rng.integers(1, num_bodies, endpoint=True) + env_ids_np = rng.choice(num_envs, size=num_envs_np, replace=False) + body_ids_np = rng.choice(num_bodies, size=num_bodies_np, replace=False) + # Convert to warp arrays + env_ids = wp.from_numpy(env_ids_np, dtype=wp.int32, device=device) + body_ids = wp.from_numpy(body_ids_np, dtype=wp.int32, device=device) + # Get random torques + torques_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + positions_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + torques = wp.from_numpy(torques_np, dtype=wp.vec3f, device=device) + positions = wp.from_numpy(positions_np, dtype=wp.vec3f, device=device) + # Add torques at positions to wrench composer + wrench_composer.add_forces_and_torques( + torques=torques, positions=positions, body_ids=body_ids, env_ids=env_ids + ) + # Add torques to hand-calculated composed torque + hand_calculated_composed_torque_np[env_ids_np[:, None], body_ids_np[None, :], :] += torques_np + # Get composed torque from wrench composer + composed_torque_np = wrench_composer.composed_torque.numpy() + assert np.allclose(composed_torque_np, hand_calculated_composed_torque_np, atol=1, rtol=1e-7) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100, 1000]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5, 10]) +def test_add_forces_and_torques_at_position(device: str, num_envs: int, num_bodies: int): + """Test adding forces and torques at local positions.""" + rng = np.random.default_rng(seed=4) + + for _ in range(10): + mock_asset = MockRigidObject(num_envs, num_bodies, device) + wrench_composer = WrenchComposer(mock_asset) + # Initialize hand-calculated composed force and torque + hand_calculated_composed_force_np = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + hand_calculated_composed_torque_np = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + for _ in range(10): + # Get random number of envs and bodies and their indices + num_envs_np = rng.integers(1, num_envs, endpoint=True) + num_bodies_np = rng.integers(1, num_bodies, endpoint=True) + env_ids_np = rng.choice(num_envs, size=num_envs_np, replace=False) + body_ids_np = rng.choice(num_bodies, size=num_bodies_np, replace=False) + # Convert to warp arrays + env_ids = wp.from_numpy(env_ids_np, dtype=wp.int32, device=device) + body_ids = wp.from_numpy(body_ids_np, dtype=wp.int32, device=device) + # Get random forces and torques + forces_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + torques_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + positions_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + forces = wp.from_numpy(forces_np, dtype=wp.vec3f, device=device) + torques = wp.from_numpy(torques_np, dtype=wp.vec3f, device=device) + positions = wp.from_numpy(positions_np, dtype=wp.vec3f, device=device) + # Add forces and torques at positions to wrench composer + wrench_composer.add_forces_and_torques( + forces=forces, torques=torques, positions=positions, body_ids=body_ids, env_ids=env_ids + ) + # Add forces to hand-calculated composed force + hand_calculated_composed_force_np[env_ids_np[:, None], body_ids_np[None, :], :] += forces_np + # Add torques to hand-calculated composed torque: torque = cross(position, force) + torque + torques_from_forces = np.cross(positions_np, forces_np) + for i in range(num_envs_np): + for j in range(num_bodies_np): + hand_calculated_composed_torque_np[env_ids_np[i], body_ids_np[j], :] += torques_from_forces[i, j, :] + hand_calculated_composed_torque_np[env_ids_np[:, None], body_ids_np[None, :], :] += torques_np + # Get composed force from wrench composer + composed_force_np = wrench_composer.composed_force.numpy() + assert np.allclose(composed_force_np, hand_calculated_composed_force_np, atol=1, rtol=1e-7) + # Get composed torque from wrench composer + composed_torque_np = wrench_composer.composed_torque.numpy() + assert np.allclose(composed_torque_np, hand_calculated_composed_torque_np, atol=1, rtol=1e-7) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100, 1000]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5, 10]) +def test_wrench_composer_reset(device: str, num_envs: int, num_bodies: int): + rng = np.random.default_rng(seed=5) + for _ in range(10): + mock_asset = MockRigidObject(num_envs, num_bodies, device) + wrench_composer = WrenchComposer(mock_asset) + # Get random number of envs and bodies and their indices + num_envs_np = rng.integers(1, num_envs, endpoint=True) + num_bodies_np = rng.integers(1, num_bodies, endpoint=True) + env_ids_np = rng.choice(num_envs, size=num_envs_np, replace=False) + body_ids_np = rng.choice(num_bodies, size=num_bodies_np, replace=False) + # Convert to warp arrays + env_ids = wp.from_numpy(env_ids_np, dtype=wp.int32, device=device) + body_ids = wp.from_numpy(body_ids_np, dtype=wp.int32, device=device) + # Get random forces and torques + forces_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + torques_np = ( + np.random.uniform(low=-100.0, high=100.0, size=(num_envs_np * num_bodies_np * 3)) + .reshape(num_envs_np, num_bodies_np, 3) + .astype(np.float32) + ) + forces = wp.from_numpy(forces_np, dtype=wp.vec3f, device=device) + torques = wp.from_numpy(torques_np, dtype=wp.vec3f, device=device) + # Add forces and torques to wrench composer + wrench_composer.add_forces_and_torques(forces=forces, torques=torques, body_ids=body_ids, env_ids=env_ids) + # Reset wrench composer + wrench_composer.reset() + # Get composed force and torque from wrench composer + composed_force_np = wrench_composer.composed_force.numpy() + composed_torque_np = wrench_composer.composed_torque.numpy() + assert np.allclose(composed_force_np, np.zeros((num_envs, num_bodies, 3)), atol=1, rtol=1e-7) + assert np.allclose(composed_torque_np, np.zeros((num_envs, num_bodies, 3)), atol=1, rtol=1e-7) + + +# ============================================================================ +# Global Frame Tests +# ============================================================================ + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5]) +def test_global_forces_with_rotation(device: str, num_envs: int, num_bodies: int): + """Test that global forces are correctly rotated to the local frame.""" + rng = np.random.default_rng(seed=10) + + for _ in range(5): + # Create random link quaternions + link_quat_np = random_unit_quaternion_np(rng, (num_envs, num_bodies)) + link_quat_torch = torch.from_numpy(link_quat_np) + + # Create mock asset with custom quaternions + mock_asset = MockRigidObject(num_envs, num_bodies, device, link_quat=link_quat_torch) + wrench_composer = WrenchComposer(mock_asset) + + # Generate random global forces for all envs and bodies + forces_global_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + forces_global = wp.from_numpy(forces_global_np, dtype=wp.vec3f, device=device) + + # Apply global forces + wrench_composer.add_forces_and_torques(forces=forces_global, is_global=True) + + # Compute expected local forces by rotating global forces by inverse quaternion + expected_forces_local = quat_rotate_inv_np(link_quat_np, forces_global_np) + + # Verify + composed_force_np = wrench_composer.composed_force.numpy() + assert np.allclose(composed_force_np, expected_forces_local, atol=1e-4, rtol=1e-5), ( + f"Global force rotation failed.\nExpected:\n{expected_forces_local}\nGot:\n{composed_force_np}" + ) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 100]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5]) +def test_global_torques_with_rotation(device: str, num_envs: int, num_bodies: int): + """Test that global torques are correctly rotated to the local frame.""" + rng = np.random.default_rng(seed=11) + + for _ in range(5): + # Create random link quaternions + link_quat_np = random_unit_quaternion_np(rng, (num_envs, num_bodies)) + link_quat_torch = torch.from_numpy(link_quat_np) + + # Create mock asset with custom quaternions + mock_asset = MockRigidObject(num_envs, num_bodies, device, link_quat=link_quat_torch) + wrench_composer = WrenchComposer(mock_asset) + + # Generate random global torques + torques_global_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + torques_global = wp.from_numpy(torques_global_np, dtype=wp.vec3f, device=device) + + # Apply global torques + wrench_composer.add_forces_and_torques(torques=torques_global, is_global=True) + + # Compute expected local torques + expected_torques_local = quat_rotate_inv_np(link_quat_np, torques_global_np) + + # Verify + composed_torque_np = wrench_composer.composed_torque.numpy() + assert np.allclose(composed_torque_np, expected_torques_local, atol=1e-4, rtol=1e-5), ( + f"Global torque rotation failed.\nExpected:\n{expected_torques_local}\nGot:\n{composed_torque_np}" + ) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 50]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5]) +def test_global_forces_at_global_position(device: str, num_envs: int, num_bodies: int): + """Test global forces at global positions with full coordinate transformation.""" + rng = np.random.default_rng(seed=12) + + for _ in range(5): + # Create random link poses + link_pos_np = rng.uniform(-10.0, 10.0, (num_envs, num_bodies, 3)).astype(np.float32) + link_quat_np = random_unit_quaternion_np(rng, (num_envs, num_bodies)) + link_pos_torch = torch.from_numpy(link_pos_np) + link_quat_torch = torch.from_numpy(link_quat_np) + + # Create mock asset + mock_asset = MockRigidObject(num_envs, num_bodies, device, link_pos=link_pos_torch, link_quat=link_quat_torch) + wrench_composer = WrenchComposer(mock_asset) + + # Generate random global forces and positions + forces_global_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + positions_global_np = rng.uniform(-10.0, 10.0, (num_envs, num_bodies, 3)).astype(np.float32) + forces_global = wp.from_numpy(forces_global_np, dtype=wp.vec3f, device=device) + positions_global = wp.from_numpy(positions_global_np, dtype=wp.vec3f, device=device) + + # Apply global forces at global positions + wrench_composer.add_forces_and_torques(forces=forces_global, positions=positions_global, is_global=True) + + # Compute expected results: + # 1. Force in local frame = quat_rotate_inv(link_quat, global_force) + expected_forces_local = quat_rotate_inv_np(link_quat_np, forces_global_np) + + # 2. Position offset in local frame = global_position - link_position (then used for torque) + position_offset_global = positions_global_np - link_pos_np + + # 3. Torque = skew(position_offset_global) @ force_global, then rotate to local + expected_torques_local = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + for i in range(num_envs): + for j in range(num_bodies): + pos_offset = position_offset_global[i, j] # global frame offset + force_local = expected_forces_local[i, j] # local frame force + # skew(pos_offset) @ force_local + expected_torques_local[i, j] = np.cross(pos_offset, force_local) + + # Verify forces + composed_force_np = wrench_composer.composed_force.numpy() + assert np.allclose(composed_force_np, expected_forces_local, atol=1e-3, rtol=1e-4), ( + f"Global force at position failed.\nExpected forces:\n{expected_forces_local}\nGot:\n{composed_force_np}" + ) + + # Verify torques + composed_torque_np = wrench_composer.composed_torque.numpy() + assert np.allclose(composed_torque_np, expected_torques_local, atol=1e-3, rtol=1e-4), ( + f"Global force at position failed.\nExpected torques:\n{expected_torques_local}\nGot:\n{composed_torque_np}" + ) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_local_vs_global_identity_quaternion(device: str): + """Test that local and global give same result with identity quaternion and zero position.""" + rng = np.random.default_rng(seed=13) + num_envs, num_bodies = 10, 5 + + # Create mock with identity pose (default) + mock_asset_local = MockRigidObject(num_envs, num_bodies, device) + mock_asset_global = MockRigidObject(num_envs, num_bodies, device) + + wrench_composer_local = WrenchComposer(mock_asset_local) + wrench_composer_global = WrenchComposer(mock_asset_global) + + # Generate random forces and torques + forces_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + torques_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + forces = wp.from_numpy(forces_np, dtype=wp.vec3f, device=device) + torques = wp.from_numpy(torques_np, dtype=wp.vec3f, device=device) + + # Apply as local + wrench_composer_local.add_forces_and_torques(forces=forces, torques=torques, is_global=False) + + # Apply as global (should be same with identity quaternion) + wrench_composer_global.add_forces_and_torques(forces=forces, torques=torques, is_global=True) + + # Results should be identical + assert np.allclose( + wrench_composer_local.composed_force.numpy(), + wrench_composer_global.composed_force.numpy(), + atol=1e-6, + ) + assert np.allclose( + wrench_composer_local.composed_torque.numpy(), + wrench_composer_global.composed_torque.numpy(), + atol=1e-6, + ) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_90_degree_rotation_global_force(device: str): + """Test global force with a known 90-degree rotation for easy verification.""" + num_envs, num_bodies = 1, 1 + + # 90-degree rotation around Z-axis: (w, x, y, z) = (cos(45°), 0, 0, sin(45°)) + # This rotates X -> Y, Y -> -X + angle = np.pi / 2 + link_quat_np = np.array([[[[np.cos(angle / 2), 0, 0, np.sin(angle / 2)]]]], dtype=np.float32).reshape(1, 1, 4) + link_quat_torch = torch.from_numpy(link_quat_np) + + mock_asset = MockRigidObject(num_envs, num_bodies, device, link_quat=link_quat_torch) + wrench_composer = WrenchComposer(mock_asset) + + # Apply force in global +X direction + force_global = np.array([[[1.0, 0.0, 0.0]]], dtype=np.float32) + force_wp = wp.from_numpy(force_global, dtype=wp.vec3f, device=device) + + wrench_composer.add_forces_and_torques(forces=force_wp, is_global=True) + + # Expected: After inverse rotation (rotate by -90° around Z), X becomes -Y + # Actually, inverse rotation of +90° around Z applied to (1,0,0) gives (0,-1,0) + expected_force_local = np.array([[[0.0, -1.0, 0.0]]], dtype=np.float32) + + composed_force_np = wrench_composer.composed_force.numpy() + assert np.allclose(composed_force_np, expected_force_local, atol=1e-5), ( + f"90-degree rotation test failed.\nExpected:\n{expected_force_local}\nGot:\n{composed_force_np}" + ) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_composition_mixed_local_and_global(device: str): + """Test that local and global forces can be composed together correctly.""" + rng = np.random.default_rng(seed=14) + num_envs, num_bodies = 5, 3 + + # Create random link quaternions + link_quat_np = random_unit_quaternion_np(rng, (num_envs, num_bodies)) + link_quat_torch = torch.from_numpy(link_quat_np) + + mock_asset = MockRigidObject(num_envs, num_bodies, device, link_quat=link_quat_torch) + wrench_composer = WrenchComposer(mock_asset) + + # Generate random local and global forces + forces_local_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + forces_global_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + + forces_local = wp.from_numpy(forces_local_np, dtype=wp.vec3f, device=device) + forces_global = wp.from_numpy(forces_global_np, dtype=wp.vec3f, device=device) + + # Add local forces first + wrench_composer.add_forces_and_torques(forces=forces_local, is_global=False) + + # Add global forces + wrench_composer.add_forces_and_torques(forces=forces_global, is_global=True) + + # Expected: local forces stay as-is, global forces get rotated, then sum + global_forces_in_local = quat_rotate_inv_np(link_quat_np, forces_global_np) + expected_total = forces_local_np + global_forces_in_local + + composed_force_np = wrench_composer.composed_force.numpy() + assert np.allclose(composed_force_np, expected_total, atol=1e-4, rtol=1e-5), ( + f"Mixed local/global composition failed.\nExpected:\n{expected_total}\nGot:\n{composed_force_np}" + ) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("num_envs", [1, 10, 50]) +@pytest.mark.parametrize("num_bodies", [1, 3, 5]) +def test_local_forces_at_local_position(device: str, num_envs: int, num_bodies: int): + """Test local forces at local positions (offset from link frame).""" + rng = np.random.default_rng(seed=15) + + for _ in range(5): + # Create random link poses (shouldn't affect local frame calculations) + link_pos_np = rng.uniform(-10.0, 10.0, (num_envs, num_bodies, 3)).astype(np.float32) + link_quat_np = random_unit_quaternion_np(rng, (num_envs, num_bodies)) + link_pos_torch = torch.from_numpy(link_pos_np) + link_quat_torch = torch.from_numpy(link_quat_np) + + mock_asset = MockRigidObject(num_envs, num_bodies, device, link_pos=link_pos_torch, link_quat=link_quat_torch) + wrench_composer = WrenchComposer(mock_asset) + + # Generate random local forces and local positions (offsets) + forces_local_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + positions_local_np = rng.uniform(-10.0, 10.0, (num_envs, num_bodies, 3)).astype(np.float32) + forces_local = wp.from_numpy(forces_local_np, dtype=wp.vec3f, device=device) + positions_local = wp.from_numpy(positions_local_np, dtype=wp.vec3f, device=device) + + # Apply local forces at local positions + wrench_composer.add_forces_and_torques(forces=forces_local, positions=positions_local, is_global=False) + + # Expected: forces stay as-is, torque = cross(position, force) + expected_forces = forces_local_np + expected_torques = np.cross(positions_local_np, forces_local_np) + + # Verify + composed_force_np = wrench_composer.composed_force.numpy() + composed_torque_np = wrench_composer.composed_torque.numpy() + + assert np.allclose(composed_force_np, expected_forces, atol=1e-4, rtol=1e-5) + assert np.allclose(composed_torque_np, expected_torques, atol=1e-4, rtol=1e-5) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_global_force_at_link_origin_no_torque(device: str): + """Test that a global force applied at the link origin produces no torque.""" + rng = np.random.default_rng(seed=16) + num_envs, num_bodies = 5, 3 + + # Create random link poses + link_pos_np = rng.uniform(-10.0, 10.0, (num_envs, num_bodies, 3)).astype(np.float32) + link_quat_np = random_unit_quaternion_np(rng, (num_envs, num_bodies)) + link_pos_torch = torch.from_numpy(link_pos_np) + link_quat_torch = torch.from_numpy(link_quat_np) + + mock_asset = MockRigidObject(num_envs, num_bodies, device, link_pos=link_pos_torch, link_quat=link_quat_torch) + wrench_composer = WrenchComposer(mock_asset) + + # Generate random global forces + forces_global_np = rng.uniform(-100.0, 100.0, (num_envs, num_bodies, 3)).astype(np.float32) + forces_global = wp.from_numpy(forces_global_np, dtype=wp.vec3f, device=device) + + # Position = link position (so offset is zero) + positions_at_link = wp.from_numpy(link_pos_np, dtype=wp.vec3f, device=device) + + # Apply global forces at link origin + wrench_composer.add_forces_and_torques(forces=forces_global, positions=positions_at_link, is_global=True) + + # Expected: force rotated to local, torque = 0 (since position offset is zero) + expected_forces = quat_rotate_inv_np(link_quat_np, forces_global_np) + expected_torques = np.zeros((num_envs, num_bodies, 3), dtype=np.float32) + + composed_force_np = wrench_composer.composed_force.numpy() + composed_torque_np = wrench_composer.composed_torque.numpy() + + assert np.allclose(composed_force_np, expected_forces, atol=1e-4, rtol=1e-5) + assert np.allclose(composed_torque_np, expected_torques, atol=1e-4, rtol=1e-5) diff --git a/source/isaaclab/test/visualization/check_scene_xr_visualization.py b/source/isaaclab/test/visualization/check_scene_xr_visualization.py index dd614082b8ea..b03fa9e88bd2 100644 --- a/source/isaaclab/test/visualization/check_scene_xr_visualization.py +++ b/source/isaaclab/test/visualization/check_scene_xr_visualization.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -70,10 +70,9 @@ def get_camera_position(): tuple: (x, y, z) camera position or None if not available """ try: - import isaacsim.core.utils.stage as stage_utils from pxr import UsdGeom - stage = stage_utils.get_current_stage() + stage = sim_utils.get_current_stage() if stage is not None: # Get the viewport camera prim camera_prim_path = "/OmniverseKit_Persp" @@ -185,28 +184,34 @@ def apply_sample_visualization(): # Display a panel on the left to display DataCollector data # Refresh periodically - XRVisualization.set_attrs({ - "left_panel_id": "/left_panel", - "left_panel_translation": Gf.Vec3f(-2, 2.6, 2), - "left_panel_updated_times": 0, - "right_panel_updated_times": 0, - }) + XRVisualization.set_attrs( + { + "left_panel_id": "/left_panel", + "left_panel_translation": Gf.Vec3f(-2, 2.6, 2), + "left_panel_updated_times": 0, + "right_panel_updated_times": 0, + } + ) XRVisualization.register_callback(TriggerType.TRIGGER_ON_PERIOD, {"period": 1.0}, _sample_update_left_panel) # Display a panel on the right to display DataCollector data # Refresh when camera position changes - XRVisualization.set_attrs({ - "right_panel_id": "/right_panel", - "right_panel_translation": Gf.Vec3f(1.5, 2, 2), - }) + XRVisualization.set_attrs( + { + "right_panel_id": "/right_panel", + "right_panel_translation": Gf.Vec3f(1.5, 2, 2), + } + ) XRVisualization.register_callback( TriggerType.TRIGGER_ON_CHANGE, {"variable_name": "right_panel_data"}, _sample_update_right_panel ) # Change error text color every second - XRVisualization.set_attrs({ - "error_text_color": 0xFF0000FF, - }) + XRVisualization.set_attrs( + { + "error_text_color": 0xFF0000FF, + } + ) XRVisualization.register_callback(TriggerType.TRIGGER_ON_UPDATE, {}, _sample_update_error_text_color) diff --git a/source/isaaclab_assets/config/extension.toml b/source/isaaclab_assets/config/extension.toml index dac5494087e0..d45724d97347 100644 --- a/source/isaaclab_assets/config/extension.toml +++ b/source/isaaclab_assets/config/extension.toml @@ -1,6 +1,6 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "0.2.3" +version = "0.2.4" # Description title = "Isaac Lab Assets" @@ -12,6 +12,7 @@ keywords = ["kit", "robotics", "assets", "isaaclab"] [dependencies] "isaaclab" = {} +"isaaclab_contrib" = {} [core] reloadable = false diff --git a/source/isaaclab_assets/docs/CHANGELOG.rst b/source/isaaclab_assets/docs/CHANGELOG.rst index b6582e77e8a2..3456213b3e8e 100644 --- a/source/isaaclab_assets/docs/CHANGELOG.rst +++ b/source/isaaclab_assets/docs/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog --------- +0.2.4 (2025-11-26) +~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Configuration for OpenArm robots used for manipulation tasks. + 0.2.3 (2025-08-11) ~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab_assets/isaaclab_assets/__init__.py b/source/isaaclab_assets/isaaclab_assets/__init__.py index 5b4a782caffa..d83e15466fc3 100644 --- a/source/isaaclab_assets/isaaclab_assets/__init__.py +++ b/source/isaaclab_assets/isaaclab_assets/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/__init__.py b/source/isaaclab_assets/isaaclab_assets/robots/__init__.py index 82a13a05e498..77bcf04d0a3e 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/__init__.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,14 @@ # Configuration for different assets. ## +from .agibot import * +from .agility import * from .allegro import * from .ant import * from .anymal import * from .cart_double_pendulum import * from .cartpole import * +from .cassie import * from .fourier import * from .franka import * from .galbot import * diff --git a/source/isaaclab_assets/isaaclab_assets/robots/agibot.py b/source/isaaclab_assets/isaaclab_assets/robots/agibot.py index 4acce179687d..c5483721d2e0 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/agibot.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/agibot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -121,7 +121,8 @@ ), # "left_Right_2_Joint" is excluded from Articulation. # "left_hand_joint1" is the driver joint, and "left_Right_1_Joint" is the mimic joint. - # "left_.*_Support_Joint" driver joint can be set optionally, to disable the driver, set stiffness and damping to 0.0 below + # "left_.*_Support_Joint" driver joint can be set optionally, to disable the driver, + # set stiffness and damping to 0.0 below "left_gripper": ImplicitActuatorCfg( joint_names_expr=["left_hand_joint1", "left_.*_Support_Joint"], effort_limit_sim={"left_hand_joint1": 10.0, "left_.*_Support_Joint": 1.0}, @@ -139,7 +140,8 @@ ), # "right_Right_2_Joint" is excluded from Articulation. # "right_hand_joint1" is the driver joint, and "right_Right_1_Joint" is the mimic joint. - # "right_.*_Support_Joint" driver joint can be set optionally, to disable the driver, set stiffness and damping to 0.0 below + # "right_.*_Support_Joint" driver joint can be set optionally, to disable the driver, + # set stiffness and damping to 0.0 below "right_gripper": ImplicitActuatorCfg( joint_names_expr=["right_hand_joint1", "right_.*_Support_Joint"], effort_limit_sim={"right_hand_joint1": 100.0, "right_.*_Support_Joint": 100.0}, diff --git a/source/isaaclab_assets/isaaclab_assets/robots/agility.py b/source/isaaclab_assets/isaaclab_assets/robots/agility.py index 090298d5996d..2c85a42ec681 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/agility.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/agility.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/allegro.py b/source/isaaclab_assets/isaaclab_assets/robots/allegro.py index abc601aa2832..0e18ef77c13c 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/allegro.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/allegro.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +15,10 @@ """ - import math import isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR diff --git a/source/isaaclab_assets/isaaclab_assets/robots/ant.py b/source/isaaclab_assets/isaaclab_assets/robots/ant.py index 16a159223e51..49798ad638dd 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/ant.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/ant.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/anymal.py b/source/isaaclab_assets/isaaclab_assets/robots/anymal.py index fd09989db78f..ac0e565513f4 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/anymal.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/anymal.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -19,14 +19,14 @@ """ -from isaaclab_assets.sensors.velodyne import VELODYNE_VLP_16_RAYCASTER_CFG - import isaaclab.sim as sim_utils from isaaclab.actuators import ActuatorNetLSTMCfg, DCMotorCfg from isaaclab.assets.articulation import ArticulationCfg from isaaclab.sensors import RayCasterCfg from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +from isaaclab_assets.sensors.velodyne import VELODYNE_VLP_16_RAYCASTER_CFG + ## # Configuration - Actuators. ## diff --git a/source/isaaclab_assets/isaaclab_assets/robots/arl_robot_1.py b/source/isaaclab_assets/isaaclab_assets/robots/arl_robot_1.py new file mode 100644 index 000000000000..6ac4b9fc55e8 --- /dev/null +++ b/source/isaaclab_assets/isaaclab_assets/robots/arl_robot_1.py @@ -0,0 +1,75 @@ +# 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 + +"""Configuration for the ARL robots. + +The following configuration parameters are available: + +* :obj:`ARL_ROBOT_1_CFG`: The ARL_Robot_1 with (TODO add motor propeller combination) +""" + +import isaaclab.sim as sim_utils +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from isaaclab_contrib.actuators import ThrusterCfg +from isaaclab_contrib.assets import MultirotorCfg + +## +# Configuration - Actuators. +## + +ARL_ROBOT_1_THRUSTER = ThrusterCfg( + thrust_range=(0.1, 10.0), + thrust_const_range=(9.26312e-06, 1.826312e-05), + tau_inc_range=(0.05, 0.08), + tau_dec_range=(0.005, 0.005), + torque_to_thrust_ratio=0.07, + thruster_names_expr=["back_left_prop", "back_right_prop", "front_left_prop", "front_right_prop"], +) + +## +# Configuration - Articulation. +## + +ARL_ROBOT_1_CFG = MultirotorCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/NTNU/ARL-Robot-1/arl_robot_1.usd", + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + retain_accelerations=False, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + ), + init_state=MultirotorCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.0), + lin_vel=(0.0, 0.0, 0.0), + ang_vel=(0.0, 0.0, 0.0), + rot=(1.0, 0.0, 0.0, 0.0), + rps={ + "back_left_prop": 200.0, + "back_right_prop": 200.0, + "front_left_prop": 200.0, + "front_right_prop": 200.0, + }, + ), + actuators={"thrusters": ARL_ROBOT_1_THRUSTER}, + rotor_directions=[1, -1, 1, -1], + allocation_matrix=[ + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [-0.13, -0.13, 0.13, 0.13], + [-0.13, 0.13, 0.13, -0.13], + [-0.07, 0.07, -0.07, 0.07], + ], +) diff --git a/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py b/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py index 06d6890f1a3f..22028f39baf2 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/cart_double_pendulum.py @@ -1,11 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Configuration for a simple inverted Double Pendulum on a Cart robot.""" - import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg diff --git a/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py b/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py index c95bf156518f..1e236eda6b93 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/cartpole.py @@ -1,11 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Configuration for a simple Cartpole robot.""" - import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg diff --git a/source/isaaclab_assets/isaaclab_assets/robots/cassie.py b/source/isaaclab_assets/isaaclab_assets/robots/cassie.py index 147af17522f4..09e75e241fed 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/cassie.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/cassie.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/fourier.py b/source/isaaclab_assets/isaaclab_assets/robots/fourier.py index b2d87b1ee8f3..58e143d11885 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/fourier.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/fourier.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 @@ The following configuration parameters are available: * :obj:`GR1T2_CFG`: The GR1T2 humanoid. -* :obj:`GR1T2_HIGH_PD_CFG`: The GR1T2 humanoid configured with high PD gains on upper body joints for pick-place manipulation tasks. +* :obj:`GR1T2_HIGH_PD_CFG`: The GR1T2 humanoid configured with high PD gains on upper + body joints for pick-place manipulation tasks. Reference: https://www.fftai.com/products-gr1 """ diff --git a/source/isaaclab_assets/isaaclab_assets/robots/franka.py b/source/isaaclab_assets/isaaclab_assets/robots/franka.py index 36d07253425b..caacf214c58f 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/franka.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/franka.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,7 +14,6 @@ Reference: https://github.com/frankaemika/franka_ros """ - import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg diff --git a/source/isaaclab_assets/isaaclab_assets/robots/galbot.py b/source/isaaclab_assets/isaaclab_assets/robots/galbot.py index cdba75d1b8bb..9827c7c8d31e 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/galbot.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/galbot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py b/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py index 927f506f2a12..42940b4fa1f4 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/humanoid.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py b/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py index b9569b57879c..84f44339a537 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/humanoid_28.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/kinova.py b/source/isaaclab_assets/isaaclab_assets/robots/kinova.py index addcb1282559..3bef3896232e 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/kinova.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/kinova.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py b/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py index d6c86bb3f15b..35b7e0b179a0 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +17,7 @@ """ import isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR diff --git a/source/isaaclab_assets/isaaclab_assets/robots/openarm.py b/source/isaaclab_assets/isaaclab_assets/robots/openarm.py new file mode 100644 index 000000000000..02743c5da915 --- /dev/null +++ b/source/isaaclab_assets/isaaclab_assets/robots/openarm.py @@ -0,0 +1,173 @@ +# 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 + +"""Configuration of OpenArm robots. + +The following configurations are available: + +* :obj:`OPENARM_BI_CFG`: OpenArm robot with two arms. +* :obj:`OPENARM_BI_HIGH_PD_CFG`: OpenArm robot with two arms and stiffer PD control. +* :obj:`OPENARM_UNI_CFG`: OpenArm robot with one arm. +* :obj:`OPENARM_UNI_HIGH_PD_CFG`: OpenArm robot with one arm and stiffer PD control. + +References: +OpenArm repositories: +* https://github.com/enactic/openarm +* https://github.com/enactic/openarm_isaac_lab + +Motor spec sheets: +* Joint 1–2 (DM-J8009P-2EC): + https://cdn.shopify.com/s/files/1/0673/6848/5000/files/DM-J8009P-2EC_User_Manual.pdf?v=1755481750 +* Joint 3–4 (DM-J4340P-2EC / DM-J4340-2EC): + https://cdn.shopify.com/s/files/1/0673/6848/5000/files/DM-J4340-2EC_User_Manual.pdf?v=1756883905 +* Joint 5–8 (DM-J4310-2EC V1.1): + https://files.seeedstudio.com/products/Damiao/DM-J4310-en.pdf +""" + +import isaaclab.sim as sim_utils +from isaaclab.actuators import ImplicitActuatorCfg +from isaaclab.assets.articulation import ArticulationCfg +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +OPENARM_BI_CFG = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/OpenArm/openarm_bimanual/openarm_bimanual.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=8, + solver_velocity_iteration_count=0, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + joint_pos={ + "openarm_left_joint.*": 0.0, + "openarm_right_joint.*": 0.0, + "openarm_left_finger_joint.*": 0.0, + "openarm_right_finger_joint.*": 0.0, + }, + ), + # spec sheet for reference + # DM-J8009P-2EC (Joint 1, 2): + # https://cdn.shopify.com/s/files/1/0673/6848/5000/files/DM-J8009P-2EC_User_Manual.pdf?v=1755481750 + # DM-J4340P-2EC, DM-J4340-2EC (Joint 3, 4): + # https://cdn.shopify.com/s/files/1/0673/6848/5000/files/DM-J4340-2EC_User_Manual.pdf?v=1756883905 + # DM-J4310-2EC V1.1 (Joint 5, 6, 7, 8): + # https://files.seeedstudio.com/products/Damiao/DM-J4310-en.pdf + actuators={ + "openarm_arm": ImplicitActuatorCfg( + joint_names_expr=[ + "openarm_left_joint[1-7]", + "openarm_right_joint[1-7]", + ], + velocity_limit_sim={ + "openarm_left_joint[1-2]": 2.175, + "openarm_right_joint[1-2]": 2.175, + "openarm_left_joint[3-4]": 2.175, + "openarm_right_joint[3-4]": 2.175, + "openarm_left_joint[5-7]": 2.61, + "openarm_right_joint[5-7]": 2.61, + }, + effort_limit_sim={ + "openarm_left_joint[1-2]": 40.0, + "openarm_right_joint[1-2]": 40.0, + "openarm_left_joint[3-4]": 27.0, + "openarm_right_joint[3-4]": 27.0, + "openarm_left_joint[5-7]": 7.0, + "openarm_right_joint[5-7]": 7.0, + }, + stiffness=80.0, + damping=4.0, + ), + "openarm_gripper": ImplicitActuatorCfg( + joint_names_expr=[ + "openarm_left_finger_joint.*", + "openarm_right_finger_joint.*", + ], + velocity_limit_sim=0.2, + effort_limit_sim=333.33, + stiffness=2e3, + damping=1e2, + ), + }, + soft_joint_pos_limit_factor=1.0, +) +"""Configuration of OpenArm Bimanual robot.""" + +OPENARM_UNI_CFG = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/OpenArm/openarm_unimanual/openarm_unimanual.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=8, + solver_velocity_iteration_count=0, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + joint_pos={ + "openarm_joint1": 1.57, + "openarm_joint2": 0.0, + "openarm_joint3": -1.57, + "openarm_joint4": 1.57, + "openarm_joint5": 0.0, + "openarm_joint6": 0.0, + "openarm_joint7": 0.0, + "openarm_finger_joint.*": 0.044, + }, + ), + actuators={ + "openarm_arm": ImplicitActuatorCfg( + joint_names_expr=["openarm_joint[1-7]"], + velocity_limit_sim={ + "openarm_joint[1-2]": 2.175, + "openarm_joint[3-4]": 2.175, + "openarm_joint[5-7]": 2.61, + }, + effort_limit_sim={ + "openarm_joint[1-2]": 40.0, + "openarm_joint[3-4]": 27.0, + "openarm_joint[5-7]": 7.0, + }, + stiffness=80.0, + damping=4.0, + ), + "openarm_gripper": ImplicitActuatorCfg( + joint_names_expr=["openarm_finger_joint.*"], + velocity_limit_sim=0.2, + effort_limit_sim=333.33, + stiffness=2e3, + damping=1e2, + ), + }, + soft_joint_pos_limit_factor=1.0, +) +"""Configuration of OpenArm Unimanual robot.""" + +OPENARM_BI_HIGH_PD_CFG = OPENARM_BI_CFG.copy() +OPENARM_BI_HIGH_PD_CFG.spawn.rigid_props.disable_gravity = True +OPENARM_BI_HIGH_PD_CFG.actuators["openarm_arm"].stiffness = 400.0 +OPENARM_BI_HIGH_PD_CFG.actuators["openarm_arm"].damping = 80.0 +OPENARM_BI_HIGH_PD_CFG.actuators["openarm_gripper"].stiffness = 2e3 +OPENARM_BI_HIGH_PD_CFG.actuators["openarm_gripper"].damping = 1e2 +"""Configuration of OpenArm Bimanual robot with stiffer PD control. + +This configuration is useful for task-space control using differential IK. +""" + +OPENARM_UNI_HIGH_PD_CFG = OPENARM_UNI_CFG.copy() +OPENARM_UNI_HIGH_PD_CFG.spawn.rigid_props.disable_gravity = True +OPENARM_UNI_HIGH_PD_CFG.actuators["openarm_arm"].stiffness = 400.0 +OPENARM_UNI_HIGH_PD_CFG.actuators["openarm_arm"].damping = 80.0 +"""Configuration of OpenArm Unimanual robot with stiffer PD control. + +This configuration is useful for task-space control using differential IK. +""" diff --git a/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py b/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py index 00397c4b7ed2..988e042fcf65 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/pick_and_place.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/quadcopter.py b/source/isaaclab_assets/isaaclab_assets/robots/quadcopter.py index 2b14039ece5a..f404a90e3f14 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/quadcopter.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/quadcopter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py b/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py index 9abc164420d1..312236d23373 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/ridgeback_franka.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py b/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py index 7a37b08bf5fc..179df09e7d81 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/sawyer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py index 79696aab0911..d13e90e3b1c0 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +15,8 @@ """ - import isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR diff --git a/source/isaaclab_assets/isaaclab_assets/robots/spot.py b/source/isaaclab_assets/isaaclab_assets/robots/spot.py index 6513484965a5..3bc98b8b2da3 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/spot.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/spot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/unitree.py b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py index 4e670b22756e..ff7685a3c607 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/unitree.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py b/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py index 4433b8242352..1026e00a9713 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +10,7 @@ * :obj:`UR10_CFG`: The UR10 arm without a gripper. * :obj:`UR10E_ROBOTIQ_GRIPPER_CFG`: The UR10E arm with Robotiq_2f_140 gripper. +* :obj:`UR10e_ROBOTIQ_2F_85_CFG`: The UR10E arm with Robotiq 2F-85 gripper. Reference: https://github.com/ros-industrial/universal_robot """ @@ -125,6 +126,7 @@ """Configuration of UR10 arm with short suction gripper.""" UR10e_ROBOTIQ_GRIPPER_CFG = UR10e_CFG.copy() +"""Configuration of UR10e arm with Robotiq_2f_140 gripper.""" UR10e_ROBOTIQ_GRIPPER_CFG.spawn.variants = {"Gripper": "Robotiq_2f_140"} UR10e_ROBOTIQ_GRIPPER_CFG.spawn.rigid_props.disable_gravity = True UR10e_ROBOTIQ_GRIPPER_CFG.init_state.joint_pos["finger_joint"] = 0.0 @@ -162,4 +164,44 @@ armature=0.0, ) + +UR10e_ROBOTIQ_2F_85_CFG = UR10e_CFG.copy() """Configuration of UR-10E arm with Robotiq_2f_140 gripper.""" +UR10e_ROBOTIQ_2F_85_CFG.spawn.variants = {"Gripper": "Robotiq_2f_85"} +UR10e_ROBOTIQ_2F_85_CFG.spawn.rigid_props.disable_gravity = True +UR10e_ROBOTIQ_2F_85_CFG.init_state.joint_pos["finger_joint"] = 0.0 +UR10e_ROBOTIQ_2F_85_CFG.init_state.joint_pos[".*_inner_finger_joint"] = 0.0 +UR10e_ROBOTIQ_2F_85_CFG.init_state.joint_pos[".*_inner_finger_knuckle_joint"] = 0.0 +UR10e_ROBOTIQ_2F_85_CFG.init_state.joint_pos[".*_outer_.*_joint"] = 0.0 +# the major actuator joint for gripper +UR10e_ROBOTIQ_2F_85_CFG.actuators["gripper_drive"] = ImplicitActuatorCfg( + joint_names_expr=["finger_joint"], # "right_outer_knuckle_joint" is its mimic joint + effort_limit_sim=10.0, + velocity_limit_sim=1.0, + stiffness=11.25, + damping=0.1, + friction=0.0, + armature=0.0, +) +# enable the gripper to grasp in a parallel manner +UR10e_ROBOTIQ_2F_85_CFG.actuators["gripper_finger"] = ImplicitActuatorCfg( + joint_names_expr=[".*_inner_finger_joint"], + effort_limit_sim=1.0, + velocity_limit_sim=1.0, + stiffness=0.2, + damping=0.001, + friction=0.0, + armature=0.0, +) +# set PD to zero for passive joints in close-loop gripper +UR10e_ROBOTIQ_2F_85_CFG.actuators["gripper_passive"] = ImplicitActuatorCfg( + joint_names_expr=[".*_inner_finger_knuckle_joint", "right_outer_knuckle_joint"], + effort_limit_sim=1.0, + velocity_limit_sim=1.0, + stiffness=0.0, + damping=0.0, + friction=0.0, + armature=0.0, +) + +"""Configuration of UR-10E arm with Robotiq 2F-85 gripper.""" diff --git a/source/isaaclab_assets/isaaclab_assets/sensors/__init__.py b/source/isaaclab_assets/isaaclab_assets/sensors/__init__.py index 67613a819002..f5f6c6ac116e 100644 --- a/source/isaaclab_assets/isaaclab_assets/sensors/__init__.py +++ b/source/isaaclab_assets/isaaclab_assets/sensors/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,4 +7,5 @@ # Configuration for different assets. ## +from .gelsight import * from .velodyne import * diff --git a/source/isaaclab_assets/isaaclab_assets/sensors/gelsight.py b/source/isaaclab_assets/isaaclab_assets/sensors/gelsight.py new file mode 100644 index 000000000000..13fe00e9d3c6 --- /dev/null +++ b/source/isaaclab_assets/isaaclab_assets/sensors/gelsight.py @@ -0,0 +1,49 @@ +# 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 + +"""Predefined configurations for GelSight tactile sensors.""" + +from isaaclab_contrib.sensors.tacsl_sensor.visuotactile_sensor_cfg import GelSightRenderCfg + +## +# Predefined Configurations +## + +GELSIGHT_R15_CFG = GelSightRenderCfg( + sensor_data_dir_name="gelsight_r15_data", + background_path="bg.jpg", + calib_path="polycalib.npz", + real_background="real_bg.npy", + image_height=320, + image_width=240, + num_bins=120, + mm_per_pixel=0.0877, +) +"""Configuration for GelSight R1.5 sensor rendering parameters. + +The GelSight R1.5 is a high-resolution tactile sensor with a 320x240 pixel tactile image. +It uses a pixel-to-millimeter ratio of 0.0877 mm/pixel. + +Reference: https://www.gelsight.com/gelsightinc-products/ +""" + +GELSIGHT_MINI_CFG = GelSightRenderCfg( + sensor_data_dir_name="gs_mini_data", + background_path="bg.jpg", + calib_path="polycalib.npz", + real_background="real_bg.npy", + image_height=240, + image_width=320, + num_bins=120, + mm_per_pixel=0.065, +) +"""Configuration for GelSight Mini sensor rendering parameters. + +The GelSight Mini is a compact tactile sensor with a 240x320 pixel tactile image. +It uses a pixel-to-millimeter ratio of 0.065 mm/pixel, providing higher spatial resolution +than the R1.5 model. + +Reference: https://www.gelsight.com/gelsightinc-products/ +""" diff --git a/source/isaaclab_assets/isaaclab_assets/sensors/velodyne.py b/source/isaaclab_assets/isaaclab_assets/sensors/velodyne.py index 4802c780c157..6cd075f4fa25 100644 --- a/source/isaaclab_assets/isaaclab_assets/sensors/velodyne.py +++ b/source/isaaclab_assets/isaaclab_assets/sensors/velodyne.py @@ -1,11 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Configuration for Velodyne LiDAR sensors.""" - from isaaclab.sensors import RayCasterCfg, patterns ## diff --git a/source/isaaclab_assets/setup.py b/source/isaaclab_assets/setup.py index 10c6330b9d61..ebd9331bd5f8 100644 --- a/source/isaaclab_assets/setup.py +++ b/source/isaaclab_assets/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,8 +6,8 @@ """Installation script for the 'isaaclab_assets' python package.""" import os -import toml +import toml from setuptools import setup # Obtain the extension data from the extension.toml file diff --git a/source/isaaclab_assets/test/test_valid_configs.py b/source/isaaclab_assets/test/test_valid_configs.py index 6c0fadb5a05b..acd68b260e54 100644 --- a/source/isaaclab_assets/test/test_valid_configs.py +++ b/source/isaaclab_assets/test/test_valid_configs.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -20,11 +20,11 @@ # Define a fixture to replace setUpClass import pytest -import isaaclab_assets as lab_assets # noqa: F401 - from isaaclab.assets import AssetBase, AssetBaseCfg from isaaclab.sim import build_simulation_context +import isaaclab_assets as lab_assets # noqa: F401 + @pytest.fixture(scope="module") def registered_entities(): diff --git a/source/isaaclab_contrib/config/extension.toml b/source/isaaclab_contrib/config/extension.toml new file mode 100644 index 000000000000..326c9faf7ee8 --- /dev/null +++ b/source/isaaclab_contrib/config/extension.toml @@ -0,0 +1,21 @@ +[package] +# Semantic Versioning is used: https://semver.org/ +version = "0.0.2" + +# Description +title = "Isaac Lab External Contributions" +description="An extension used to stage and integrate externally contributed features and implementations." +readme = "docs/README.md" +repository = "https://github.com/isaac-sim/IsaacLab" +category = "robotics" +keywords = ["kit", "robotics", "assets", "isaaclab"] + +[dependencies] +"isaaclab" = {} + +[core] +reloadable = false + +# Main python module this extension provides. +[[python.module]] +name = "isaaclab_contrib" diff --git a/source/isaaclab_contrib/docs/CHANGELOG.rst b/source/isaaclab_contrib/docs/CHANGELOG.rst new file mode 100644 index 000000000000..a5e0bf4c2ef4 --- /dev/null +++ b/source/isaaclab_contrib/docs/CHANGELOG.rst @@ -0,0 +1,20 @@ +Changelog +--------- + +0.0.2 (2026-01-28) +~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added :mod:`isaaclab_contrib.sensors.tacsl_sensor` module with the TacSL tactile sensor implementation + from :cite:t:`si2022taxim`. + + +0.0.1 (2025-12-17) +~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added initial implementation for multi rotor systems. diff --git a/source/isaaclab_contrib/docs/README.md b/source/isaaclab_contrib/docs/README.md new file mode 100644 index 000000000000..1861b593bd50 --- /dev/null +++ b/source/isaaclab_contrib/docs/README.md @@ -0,0 +1,506 @@ +# Isaac Lab: Community Contributions + +This extension (`isaaclab_contrib`) provides a collection of community-contributed components for Isaac Lab. These contributions extend the core framework with specialized robot types, actuator models, sensors, and other features that are not yet part of the main Isaac Lab package but are actively maintained and supported by the community. + +## Overview + +The `isaaclab_contrib` package serves as an incubator for experimental and specialized features that: + +- Extend Isaac Lab's capabilities for specific robot types or use cases +- Provide domain-specific actuator models and control interfaces +- Offer specialized MDP components for reinforcement learning tasks +- May eventually be integrated into the core Isaac Lab framework + +## Current Contributions + +### Multirotor Systems + +Comprehensive support for multirotor vehicles (drones, quadcopters, hexacopters, octocopters, etc.) including: + +- **Assets**: `Multirotor` articulation class with thruster-based control +- **Actuators**: `Thruster` model with realistic motor dynamics +- **MDP Components**: `ThrustAction` terms for RL control + +See the [Multirotor Systems](#multirotor-systems-detailed) section below for detailed documentation. + +### TacSL Tactile Sensor + +Support for tactile sensor from [Akinola et al., 2025](https://arxiv.org/abs/2408.06506). +It uses the Taxim model from [Si et al., 2022](https://arxiv.org/abs/2109.04027) to render the tactile images. + +See the [TacSL Tactile Sensor](#tacsl-tactile-sensor-detailed) section below for detailed documentation. + +## Extension Structure + +The extension follows Isaac Lab's standard package structure: + +```tree +isaaclab_contrib/ +├── actuators/ # Contributed actuator models +│ ├── thruster.py # Thruster actuator for multirotors +│ └── thruster_cfg.py +├── assets/ # Contributed asset classes +│ └── multirotor/ # Multirotor asset implementation +├── mdp/ # MDP components for RL +│ └── actions/ # Action terms +├── sensors/ # Contributed sensor classes +│ └── tacsl_sensor/ # TacSL tactile sensor implementation +└── utils/ # Utility functions and types +``` + +## Installation and Usage + +The `isaaclab_contrib` package is included with Isaac Lab. To use contributed components: + +```python +# Import multirotor components +from isaaclab_contrib.assets import Multirotor, MultirotorCfg +from isaaclab_contrib.actuators import Thruster, ThrusterCfg +from isaaclab_contrib.mdp.actions import ThrustActionCfg +from isaaclab_contrib.sensors import VisuoTactileSensor, VisuoTactileSensorCfg +``` + +--- + +## Multirotor Systems (Detailed) + +This section provides detailed documentation for the multirotor contribution, which enables simulation and control of multirotor aerial vehicles in Isaac Lab. + +
+ +### Features + +The multirotor system includes: + +#### Assets + +- **`Multirotor`**: A specialized articulation class that extends the base `Articulation` class to support multirotor systems with thruster actuators + - Manages multiple thruster actuators as a group + - Applies thrust forces at specific body locations + - Uses allocation matrices for control allocation + - Provides thruster-specific state information through `MultirotorData` + +#### Actuators + +- **`Thruster`**: A low-level motor/thruster dynamics model with realistic response characteristics: + - **Asymmetric rise and fall time constants**: Models different spin-up/spin-down rates + - **Thrust limits**: Configurable minimum and maximum thrust constraints + - **Integration schemes**: Euler or RK4 integration methods + - **Dynamic response**: Simulates motor transient behavior + +#### MDP Components + +- **`ThrustAction`**: Action terms specifically designed for multirotor control: + - Direct thrust commands to individual thrusters or groups + - Flexible preprocessing (scaling, offsetting, clipping) + - Automatic hover thrust offset computation + - Integrates with Isaac Lab's MDP framework for RL tasks + +### Quick Start + +#### Creating a Multirotor Asset + +```python +import isaaclab.sim as sim_utils +from isaaclab_contrib.assets import MultirotorCfg +from isaaclab_contrib.actuators import ThrusterCfg + +# Define thruster actuator configuration +thruster_cfg = ThrusterCfg( + thruster_names_expr=["rotor_[0-3]"], # Match rotors 0-3 + thrust_range=(0.0, 10.0), # Min and max thrust in Newtons + rise_time_constant=0.12, # Time constant for thrust increase (120ms) + fall_time_constant=0.25, # Time constant for thrust decrease (250ms) +) + +# Create multirotor configuration +multirotor_cfg = MultirotorCfg( + prim_path="/World/envs/env_.*/Quadcopter", + spawn=sim_utils.UsdFileCfg( + usd_path="path/to/quadcopter.usd", + ), + init_state=MultirotorCfg.InitialStateCfg( + pos=(0.0, 0.0, 1.0), # Start 1m above ground + rps={".*": 110.0}, # All thrusters at 110 RPS (hover) + ), + actuators={ + "thrusters": thruster_cfg, + }, + allocation_matrix=[ # 6x4 matrix for quadcopter + [1.0, 1.0, 1.0, 1.0], # Total vertical thrust + [0.0, 0.0, 0.0, 0.0], # Lateral force X + [0.0, 0.0, 0.0, 0.0], # Lateral force Y + [0.0, 0.13, 0.0, -0.13], # Roll torque + [-0.13, 0.0, 0.13, 0.0], # Pitch torque + [0.01, -0.01, 0.01, -0.01], # Yaw torque + ], + rotor_directions=[1, -1, 1, -1], # Alternating CW/CCW +) +``` + +#### Using Thrust Actions in RL Environments + +```python +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.utils import configclass +from isaaclab_contrib.mdp.actions import ThrustActionCfg + +@configclass +class QuadcopterEnvCfg(ManagerBasedRLEnvCfg): + # ... scene, observations, rewards, etc. ... + + @configclass + class ActionsCfg: + # Normalized thrust control around hover + thrust = ThrustActionCfg( + asset_name="robot", + scale=2.0, # Actions in [-1,1] become [-2,2] N deviation + use_default_offset=True, # Add hover thrust from config + clip={".*": (0.0, 10.0)}, # Constrain final thrust to [0, 10] N + ) + + actions = ActionsCfg() +``` + +### Key Concepts + +#### Allocation Matrix + +The allocation matrix maps individual thruster forces to a 6D wrench (force + torque) applied to the multirotor's base link: + +``` +wrench = allocation_matrix @ thrust_vector +``` + +Where: +- `wrench`: [Fx, Fy, Fz, Tx, Ty, Tz]ᵀ (6D body wrench) +- `allocation_matrix`: 6 × N matrix (6 DOF, N thrusters) +- `thrust_vector`: [T₁, T₂, ..., Tₙ]ᵀ (N thruster forces) + +The matrix encodes the geometric configuration of thrusters including positions, orientations, and moment arms. + +#### Thruster Dynamics + +The `Thruster` actuator model simulates realistic motor response with asymmetric first-order dynamics: + +``` +dT/dt = (T_target - T_current) / τ +``` + +Where τ is the time constant (different for rise vs. fall): +- **Rise Time (τ_rise)**: How quickly thrust increases when commanded (typically slower) +- **Fall Time (τ_fall)**: How quickly thrust decreases when commanded (typically faster) +- **Thrust Limits**: Physical constraints [T_min, T_max] enforced after integration + +This asymmetry reflects real-world motor behavior primarily caused by ESC (Electronic Speed Controller) response and propeller aerodynamics, which result in slower spin-up (thrust increase) than spin-down. While rotor inertia affects both acceleration and deceleration equally, it is not the main cause of the asymmetric response. + +#### Thruster Control Modes + +The multirotor system supports different control approaches: + +1. **Direct Thrust Control**: Directly command thrust forces/RPS +2. **Normalized Control**: Commands as deviations from hover thrust +3. **Differential Control**: Small adjustments around equilibrium + +The `ThrustAction` term provides flexible preprocessing to support all modes through scaling and offsetting. + +
+ +### Demo Script + +A complete demonstration of quadcopter simulation is available: + +```bash +# Run quadcopter demo +./isaaclab.sh -p scripts/demos/quadcopter.py +``` + +## TacSL Tactile Sensor (Detailed) + +This section provides detailed documentation for the TacSL tactile sensor contribution, which enables GPU-based simulation of vision-based tactile sensors in Isaac Lab. The implementation is based on the TacSL framework from [Akinola et al., 2025](https://arxiv.org/abs/2408.06506) and uses the Taxim model from [Si et al., 2022](https://arxiv.org/abs/2109.04027) for rendering tactile images. + +
+ +### Features + +The TacSL tactile sensor system includes: + +#### Sensor Capabilities + +- **`VisuoTactileSensor`**: A specialized sensor class that simulates vision-based tactile sensors with elastomer deformation + - **Camera-based RGB sensing**: Renders realistic tactile images showing surface deformation and contact patterns + - **Force field sensing**: Computes per-taxel normal and shear forces for contact-rich manipulation + - **GPU-accelerated rendering**: Leverages GPU for efficient tactile image generation + - **SDF-based contact detection**: Uses signed distance fields for accurate geometry-elastomer interaction + +#### Configuration Options + +- **Elastomer Properties**: + - Configurable tactile array size (rows × columns of taxels) + - Adjustable tactile margin for sensor boundaries + - Compliant contact parameters (stiffness, damping) + +- **Physics Parameters**: + - Normal contact stiffness: Controls elastomer compression response + - Tangential stiffness: Models lateral resistance to sliding + - Friction coefficient: Defines surface friction properties + +- **Visualization & Debug**: + - Trimesh visualization of tactile contact points + - SDF closest point visualization + - Debug rendering of sensor point cloud + +### Quick Start + +#### Creating a Tactile Sensor + +```python +import isaaclab.sim as sim_utils +from isaaclab.sensors import TiledCameraCfg + +from isaaclab_contrib.sensors.tacsl_sensor import VisuoTactileSensorCfg + +from isaaclab_assets.sensors import GELSIGHT_R15_CFG + +# Define tactile sensor configuration +tactile_sensor_cfg = VisuoTactileSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/elastomer/tactile_sensor", + history_length=0, + debug_vis=False, + + # Sensor rendering configuration + render_cfg=GELSIGHT_R15_CFG, # Use GelSight R15 sensor parameters + + # Enable RGB and/or force field sensing + enable_camera_tactile=True, # RGB tactile images + enable_force_field=True, # Force field data + + # Elastomer configuration + tactile_array_size=(20, 25), # 20×25 taxel array + tactile_margin=0.003, # 3mm sensor margin + + # Contact object configuration + contact_object_prim_path_expr="{ENV_REGEX_NS}/contact_object", + + # Force field physics parameters + normal_contact_stiffness=1.0, # Normal stiffness (N/mm) + friction_coefficient=2.0, # Surface friction + tangential_stiffness=0.1, # Tangential stiffness + + # Camera configuration (must match render_cfg dimensions) + camera_cfg=TiledCameraCfg( + prim_path="{ENV_REGEX_NS}/Robot/elastomer_tip/cam", + height=GELSIGHT_R15_CFG.image_height, + width=GELSIGHT_R15_CFG.image_width, + data_types=["distance_to_image_plane"], + spawn=None, # Camera already exists in USD + ), +) +``` + +#### Setting Up the Robot Asset with Compliant Contact + +```python +from isaaclab.assets import ArticulationCfg + +robot_cfg = ArticulationCfg( + prim_path="{ENV_REGEX_NS}/Robot", + spawn=sim_utils.UsdFileWithCompliantContactCfg( + usd_path="path/to/gelsight_finger.usd", + + # Compliant contact parameters for elastomer + compliant_contact_stiffness=100.0, # Elastomer stiffness + compliant_contact_damping=10.0, # Elastomer damping + physics_material_prim_path="elastomer", # Prim with compliant contact + + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=12, + solver_velocity_iteration_count=1, + ), + collision_props=sim_utils.CollisionPropertiesCfg( + contact_offset=0.001, + rest_offset=-0.0005, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.5), + ), + actuators={}, +) +``` + +#### Accessing Tactile Data + +```python +# In your simulation loop +scene.update(sim_dt) + +# Access tactile sensor data +tactile_data = scene["tactile_sensor"].data + +# RGB tactile image (if enabled) +if tactile_data.tactile_rgb_image is not None: + rgb_images = tactile_data.tactile_rgb_image # Shape: (num_envs, height, width, 3) + +# Force field data (if enabled) +if tactile_data.tactile_normal_force is not None: + normal_forces = tactile_data.tactile_normal_force # Shape: (num_envs * rows * cols,) + shear_forces = tactile_data.tactile_shear_force # Shape: (num_envs * rows * cols, 2) + + # Reshape to tactile array dimensions + num_envs = scene.num_envs + rows, cols = scene["tactile_sensor"].cfg.tactile_array_size + normal_forces = normal_forces.view(num_envs, rows, cols) + shear_forces = shear_forces.view(num_envs, rows, cols, 2) +``` + +### Key Concepts + +#### Sensor Modalities + +The TacSL sensor supports two complementary sensing modalities: + +1. **Camera-Based RGB Sensing** (`enable_camera_tactile=True`): + - Uses depth information from a camera inside the elastomer + - Renders realistic tactile images showing contact patterns and deformation + - Employs the Taxim rendering model for physically-based appearance + - Outputs RGB images that mimic real GelSight/DIGIT sensors + +2. **Force Field Sensing** (`enable_force_field=True`): + - Computes forces at each taxel (tactile element) in the array + - Provides normal forces (compression) and shear forces (tangential) + - Uses SDF-based contact detection with contact objects + - Enables direct force-based manipulation strategies + +#### Compliant Contact Model + +The sensor uses PhysX compliant contact for realistic elastomer deformation: + +- **Compliant Contact Stiffness**: Controls how much the elastomer compresses under load (higher = stiffer) +- **Compliant Contact Damping**: Controls energy dissipation during contact (affects bounce/settling) +- **Physics Material**: Specified prim (e.g., "elastomer") that has compliant contact enabled + +This allows the elastomer surface to deform realistically when contacting objects, which is essential for accurate tactile sensing. + +#### Tactile Array Configuration + +The sensor discretizes the elastomer surface into a grid of taxels: + +``` +tactile_array_size = (rows, cols) # e.g., (20, 25) = 500 taxels +``` + +- Each taxel corresponds to a point on the elastomer surface +- Forces are computed per-taxel for force field sensing +- The tactile_margin parameter defines the boundary region to exclude from sensing +- Higher resolution (more taxels) provides finer spatial detail but increases computation + +#### SDF-Based Contact Detection + +For force field sensing, the sensor uses Signed Distance Fields (SDFs): + +- Contact objects must have SDF collision meshes +- SDF provides distance and gradient information for force computation +- **Note**: Simple shape primitives (cubes, spheres spawned via `CuboidCfg`) cannot generate SDFs +- Use USD mesh assets for contact objects when force field sensing is required + +#### Sensor Rendering Pipeline + +The RGB tactile rendering follows this pipeline: + +1. **Initial Render**: Captures the reference state (no contact) +2. **Depth Capture**: Camera measures depth to elastomer surface during contact +3. **Deformation Computation**: Compares current depth to reference depth +4. **Taxim Rendering**: Generates RGB image based on deformation field +5. **Output**: Realistic tactile image showing contact geometry and patterns + +#### Physics Simulation Parameters + +For accurate tactile sensing, configure PhysX parameters: + +```python +sim_cfg = sim_utils.SimulationCfg( + dt=0.005, # 5ms timestep for stable contact simulation + physx=sim_utils.PhysxCfg( + gpu_collision_stack_size=2**30, # Increase for contact-rich scenarios + ), +) +``` + +Also ensure high solver iteration counts for the robot: + +```python +solver_position_iteration_count=12 # Higher = more accurate contact resolution +solver_velocity_iteration_count=1 +``` + +### Performance Considerations + +- **GPU Acceleration**: Tactile rendering is GPU-accelerated for efficiency +- **Multiple Sensors**: Can simulate multiple tactile sensors across parallel environments +- **Timing Analysis**: Use `sensor.get_timing_summary()` to profile rendering performance +- **SDF Computation**: Initial SDF generation may take time for complex meshes + +
+ +### Demo Script + +A complete demonstration of TacSL tactile sensor is available: + +```bash +# Run TacSL tactile sensor demo with RGB and force field sensing +./isaaclab.sh -p scripts/demos/sensors/tacsl_sensor.py \ + --use_tactile_rgb \ + --use_tactile_ff \ + --num_envs 16 \ + --contact_object_type nut + +# Save visualization data +./isaaclab.sh -p scripts/demos/sensors/tacsl_sensor.py \ + --use_tactile_rgb \ + --use_tactile_ff \ + --save_viz \ + --save_viz_dir tactile_output +``` + +--- + +## Testing + +The extension includes comprehensive unit tests for all contributed components: + +```bash +# Test multirotor components +python -m pytest source/isaaclab_contrib/test/assets/test_multirotor.py +python -m pytest source/isaaclab_contrib/test/actuators/test_thruster.py + +# Run all contrib tests +python -m pytest source/isaaclab_contrib/test/ +``` + +## Contributing + +We welcome community contributions to `isaaclab_contrib`! If you have developed: + +- Specialized robot asset classes +- Novel actuator models +- Custom MDP components +- Domain-specific utilities + +Please follow the Isaac Lab contribution guidelines and open a pull request. Contributions should: + +1. Follow the existing package structure +2. Include comprehensive documentation (docstrings, examples) +3. Provide unit tests +4. Be well-tested with Isaac Lab's simulation framework + +For more information, see the [Isaac Lab Contributing Guide](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html). + +## License + +This extension follows the same BSD-3-Clause license as Isaac Lab. See the LICENSE file for details. diff --git a/source/isaaclab_contrib/isaaclab_contrib/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/__init__.py new file mode 100644 index 000000000000..aef3b737ed8c --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/__init__.py @@ -0,0 +1,24 @@ +# 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 + +"""Package for externally contributed components for Isaac Lab. + +This package provides externally contributed components for Isaac Lab, such as multirotors. +These components are not part of the core Isaac Lab framework yet, but are planned to be added +in the future. They are contributed by the community to extend the capabilities of Isaac Lab. +""" + +import os +import toml + +# Conveniences to other module directories via relative paths +ISAACLAB_CONTRIB_EXT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) +"""Path to the extension source directory.""" + +ISAACLAB_CONTRIB_METADATA = toml.load(os.path.join(ISAACLAB_CONTRIB_EXT_DIR, "config", "extension.toml")) +"""Extension metadata dictionary parsed from the extension.toml file.""" + +# Configure the module-level variables +__version__ = ISAACLAB_CONTRIB_METADATA["package"]["version"] diff --git a/source/isaaclab_contrib/isaaclab_contrib/actuators/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/actuators/__init__.py new file mode 100644 index 000000000000..7d0cbbc80882 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/actuators/__init__.py @@ -0,0 +1,14 @@ +# 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 + +"""Sub-package for thruster actuator models. + +This package provides actuator models specifically designed for multirotor thrusters. +The thruster actuator simulates realistic motor/propeller dynamics including asymmetric +rise and fall time constants, thrust limits, and dynamic response characteristics. +""" + +from .thruster import Thruster +from .thruster_cfg import ThrusterCfg diff --git a/source/isaaclab_contrib/isaaclab_contrib/actuators/thruster.py b/source/isaaclab_contrib/isaaclab_contrib/actuators/thruster.py new file mode 100644 index 000000000000..036a817fbfbd --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/actuators/thruster.py @@ -0,0 +1,229 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils + +from isaaclab_contrib.utils.types import MultiRotorActions + +if TYPE_CHECKING: + from .thruster_cfg import ThrusterCfg + + +class Thruster: + """Low-level motor/thruster dynamics with separate rise/fall time constants. + + Integration scheme is Euler or RK4. All internal buffers are shaped (num_envs, num_motors). + Units: thrust [N], rates [N/s], time [s]. + """ + + computed_thrust: torch.Tensor + """The computed thrust for the actuator group. Shape is (num_envs, num_thrusters).""" + + applied_thrust: torch.Tensor + """The applied thrust for the actuator group. Shape is (num_envs, num_thrusters). + + This is the thrust obtained after clipping the :attr:`computed_thrust` based on the + actuator characteristics. + """ + + cfg: ThrusterCfg + + def __init__( + self, + cfg: ThrusterCfg, + thruster_names: list[str], + thruster_ids: slice | torch.Tensor, + num_envs: int, + device: str, + init_thruster_rps: torch.Tensor, + ): + """Construct buffers and sample per-motor parameters. + + Args: + cfg: Thruster configuration. + thruster_names: List of thruster names belonging to this group. + thruster_ids: Slice or tensor of indices into the articulation thruster array. + num_envs: Number of parallel/vectorized environments. + device: PyTorch device string or device identifier. + init_thruster_rps: Initial per-thruster rotations-per-second tensor used when + the configuration uses RPM-based thrust modelling. + """ + self.cfg = cfg + self._num_envs = num_envs + self._device = device + self._thruster_names = thruster_names + self._thruster_indices = thruster_ids + self._init_thruster_rps = init_thruster_rps + + # Range tensors, shaped (num_envs, 2, num_motors); [:,0,:]=min, [:,1,:]=max + self.num_motors = len(thruster_names) + self.thrust_r = torch.tensor(cfg.thrust_range).to(self._device) + self.tau_inc_r = torch.tensor(cfg.tau_inc_range).to(self._device) + self.tau_dec_r = torch.tensor(cfg.tau_dec_range).to(self._device) + + self.max_rate = torch.tensor(cfg.max_thrust_rate).expand(self._num_envs, self.num_motors).to(self._device) + + self.max_thrust = self.cfg.thrust_range[1] + self.min_thrust = self.cfg.thrust_range[0] + + # State & randomized per-motor parameters + self.tau_inc_s = math_utils.sample_uniform(*self.tau_inc_r, (self._num_envs, self.num_motors), self._device) + self.tau_dec_s = math_utils.sample_uniform(*self.tau_dec_r, (self._num_envs, self.num_motors), self._device) + self.thrust_const_r = torch.tensor(cfg.thrust_const_range, device=self._device, dtype=torch.float32) + self.thrust_const = math_utils.sample_uniform( + *self.thrust_const_r, (self._num_envs, self.num_motors), self._device + ).clamp(min=1e-6) + + self.curr_thrust = self.thrust_const * (self._init_thruster_rps.to(self._device).float() ** 2) + + # Mixing factor (discrete vs continuous form) + if self.cfg.use_discrete_approximation: + self.mixing_factor_function = self.discrete_mixing_factor + else: + self.mixing_factor_function = self.continuous_mixing_factor + + # Choose stepping kernel once (avoids per-step branching) + if self.cfg.integration_scheme == "euler": + self._step_thrust = self.compute_thrust_with_rpm_time_constant + elif self.cfg.integration_scheme == "rk4": + self._step_thrust = self.compute_thrust_with_rpm_time_constant_rk4 + else: + raise ValueError("integration scheme unknown") + + @property + def num_thrusters(self) -> int: + """Number of actuators in the group.""" + return len(self._thruster_names) + + @property + def thruster_names(self) -> list[str]: + """Articulation's thruster names that are part of the group.""" + return self._thruster_names + + @property + def thruster_indices(self) -> slice | torch.Tensor: + """Articulation's thruster indices that are part of the group. + + Note: + If :obj:`slice(None)` is returned, then the group contains all the thrusters in the articulation. + We do this to avoid unnecessary indexing of the thrusters for performance reasons. + """ + return self._thruster_indices + + def compute(self, control_action: MultiRotorActions) -> MultiRotorActions: + """Advance the thruster state one step. + + Applies saturation, chooses rise/fall tau per motor, computes mixing factor, + and integrates with the selected kernel. + + Args: + control_action: (num_envs, num_thrusters) commanded per-thruster thrust [N]. + + Returns: + (num_envs, num_thrusters) updated thrust state [N]. + + """ + des_thrust = control_action.thrusts + des_thrust = torch.clamp(des_thrust, *self.thrust_r) + + thrust_decrease_mask = torch.sign(self.curr_thrust) * torch.sign(des_thrust - self.curr_thrust) + motor_tau = torch.where(thrust_decrease_mask < 0, self.tau_dec_s, self.tau_inc_s) + mixing = self.mixing_factor_function(motor_tau) + + self.curr_thrust[:] = self._step_thrust(des_thrust, self.curr_thrust, mixing) + + self.computed_thrust = self.curr_thrust + self.applied_thrust = torch.clamp(self.computed_thrust, self.min_thrust, self.max_thrust) + + control_action.thrusts = self.applied_thrust + + return control_action + + def reset_idx(self, env_ids=None) -> None: + """Re-sample parameters and reinitialize state. + + Args: + env_ids: Env indices to reset. If ``None``, resets all envs. + """ + if env_ids is None: + env_ids = slice(None) + + if isinstance(env_ids, slice): + num_resets = self._num_envs + else: + num_resets = len(env_ids) + + self.tau_inc_s[env_ids] = math_utils.sample_uniform( + *self.tau_inc_r, + (num_resets, self.num_motors), + self._device, + ) + self.tau_dec_s[env_ids] = math_utils.sample_uniform( + *self.tau_dec_r, + (num_resets, self.num_motors), + self._device, + ) + self.thrust_const[env_ids] = math_utils.sample_uniform( + *self.thrust_const_r, + (num_resets, self.num_motors), + self._device, + ) + self.curr_thrust[env_ids] = self.thrust_const[env_ids] * self._init_thruster_rps[env_ids] ** 2 + + def reset(self, env_ids: Sequence[int]) -> None: + """Reset all envs.""" + self.reset_idx(env_ids) + + def motor_model_rate(self, error: torch.Tensor, mixing_factor: torch.Tensor): + return torch.clamp(mixing_factor * (error), -self.max_rate, self.max_rate) + + def rk4_integration(self, error: torch.Tensor, mixing_factor: torch.Tensor): + k1 = self.motor_model_rate(error, mixing_factor) + k2 = self.motor_model_rate(error + 0.5 * self.cfg.dt * k1, mixing_factor) + k3 = self.motor_model_rate(error + 0.5 * self.cfg.dt * k2, mixing_factor) + k4 = self.motor_model_rate(error + self.cfg.dt * k3, mixing_factor) + return (self.cfg.dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4) + + def discrete_mixing_factor(self, time_constant: torch.Tensor): + return 1.0 / (self.cfg.dt + time_constant) + + def continuous_mixing_factor(self, time_constant: torch.Tensor): + return 1.0 / time_constant + + def compute_thrust_with_rpm_time_constant( + self, + des_thrust: torch.Tensor, + curr_thrust: torch.Tensor, + mixing_factor: torch.Tensor, + ): + # Avoid negative or NaN values inside sqrt by clamping the ratio to >= 0. + current_ratio = torch.clamp(curr_thrust / self.thrust_const, min=0.0) + desired_ratio = torch.clamp(des_thrust / self.thrust_const, min=0.0) + current_rpm = torch.sqrt(current_ratio) + desired_rpm = torch.sqrt(desired_ratio) + rpm_error = desired_rpm - current_rpm + current_rpm += self.motor_model_rate(rpm_error, mixing_factor) * self.cfg.dt + return self.thrust_const * current_rpm**2 + + def compute_thrust_with_rpm_time_constant_rk4( + self, + des_thrust: torch.Tensor, + curr_thrust: torch.Tensor, + mixing_factor: torch.Tensor, + ) -> torch.Tensor: + current_ratio = torch.clamp(curr_thrust / self.thrust_const, min=0.0) + desired_ratio = torch.clamp(des_thrust / self.thrust_const, min=0.0) + current_rpm = torch.sqrt(current_ratio) + desired_rpm = torch.sqrt(desired_ratio) + rpm_error = desired_rpm - current_rpm + current_rpm += self.rk4_integration(rpm_error, mixing_factor) + return self.thrust_const * current_rpm**2 diff --git a/source/isaaclab_contrib/isaaclab_contrib/actuators/thruster_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/actuators/thruster_cfg.py new file mode 100644 index 000000000000..29072f421abb --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/actuators/thruster_cfg.py @@ -0,0 +1,62 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING +from typing import Literal + +from isaaclab.utils import configclass + +from .thruster import Thruster + + +@configclass +class ThrusterCfg: + """Configuration for thruster actuator groups. + + This config defines per-actuator-group parameters used by the low-level + thruster/motor models (time-constants, thrust ranges, integration scheme, + and initial state specifications). Fields left as ``MISSING`` are required + and must be provided by the user configuration. + """ + + class_type: type[Thruster] = Thruster + """Concrete Python class that consumes this config.""" + + dt: float = MISSING + """Simulation/integration timestep used by the thruster update [s].""" + + thrust_range: tuple[float, float] = MISSING + """Per-motor thrust clamp range [N]: values are clipped to this interval.""" + + max_thrust_rate: float = 100000.0 + """Per-motor thrust slew-rate limit applied inside the first-order model [N/s].""" + + thrust_const_range: tuple[float, float] = MISSING + """Range for thrust coefficient :math:`k_f` [N/(rps²)].""" + + tau_inc_range: tuple[float, float] = MISSING + """Range of time constants when commanded output is **increasing** (rise dynamics) [s].""" + + tau_dec_range: tuple[float, float] = MISSING + """Range of time constants when commanded output is **decreasing** (fall dynamics) [s].""" + + torque_to_thrust_ratio: float = MISSING + """Yaw-moment coefficient converting thrust to motor torque about +Z [N·m per N]. + Used as ``tau_z = torque_to_thrust_ratio * thrust_z * direction``. + """ + + use_discrete_approximation: bool = True + """ + Determines how the actuator/motor mixing factor is computed. Defaults to True. + + If True, uses the discrete-time factor ``1 / (dt + tau)``, accounting for the control loop timestep. + If False, uses the continuous-time factor ``1 / tau``. + """ + + integration_scheme: Literal["rk4", "euler"] = "rk4" + """Numerical integrator for the first-order model. Defaults to 'rk4'.""" + + thruster_names_expr: list[str] = MISSING + """Articulation's joint names that are part of the group.""" diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/assets/__init__.py new file mode 100644 index 000000000000..8c40124e72ac --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/__init__.py @@ -0,0 +1,14 @@ +# 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 + +"""Sub-package for externally contributed assets. + +This package provides specialized asset classes for simulating externally contributed +robots in Isaac Lab, such as multirotors. These assets are not part of the core +Isaac Lab framework yet, but are planned to be added in the future. They are +contributed by the community to extend the capabilities of Isaac Lab. +""" + +from .multirotor import Multirotor, MultirotorCfg, MultirotorData diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/__init__.py new file mode 100644 index 000000000000..3ef1b482d05b --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/__init__.py @@ -0,0 +1,46 @@ +# 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 + +"""Sub-module for multirotor assets. + +This module provides specialized classes for simulating multirotor vehicles (drones, +quadcopters, hexacopters, etc.) in Isaac Lab. It extends the base articulation +framework to support thrust-based control through individual rotor/propeller actuators. + +Key Components: + - :class:`Multirotor`: Asset class for multirotor vehicles with thruster control + - :class:`MultirotorCfg`: Configuration class for multirotors + - :class:`MultirotorData`: Data container for multirotor state information + +Example: + .. code-block:: python + + from isaaclab_contrib.assets import Multirotor, MultirotorCfg + from isaaclab_contrib.actuators import ThrusterCfg + import isaaclab.sim as sim_utils + + # Configure multirotor + cfg = MultirotorCfg( + prim_path="/World/Robot", + spawn=sim_utils.UsdFileCfg(usd_path="path/to/quadcopter.usd"), + actuators={ + "thrusters": ThrusterCfg( + thruster_names_expr=["rotor_[0-3]"], + thrust_range=(0.0, 10.0), + ) + }, + ) + + # Create multirotor instance + multirotor = Multirotor(cfg) + +.. seealso:: + - :mod:`isaaclab_contrib.actuators`: Thruster actuator models + - :mod:`isaaclab_contrib.mdp.actions`: Thrust action terms for RL +""" + +from .multirotor import Multirotor +from .multirotor_cfg import MultirotorCfg +from .multirotor_data import MultirotorData diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py new file mode 100644 index 000000000000..6f8800c32215 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor.py @@ -0,0 +1,565 @@ +# 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 + +# Flag for pyright to ignore type errors in this file. +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.string as string_utils +from isaaclab.assets.articulation import Articulation + +from isaaclab_contrib.actuators import Thruster +from isaaclab_contrib.utils.types import MultiRotorActions + +from .multirotor_data import MultirotorData + +if TYPE_CHECKING: + from .multirotor_cfg import MultirotorCfg + +# import logger +logger = logging.getLogger(__name__) + + +class Multirotor(Articulation): + """A multirotor articulation asset class. + + This class extends the base :class:`~isaaclab.assets.Articulation` class to support multirotor vehicles + (such as quadcopters, hexacopters, and octocopters) with thruster actuators that apply forces + at specific body locations. It is based on the implementation from :cite:t:`kulkarni2025aerialgym`. + + Unlike standard articulations that use joint-based control, multirotors are controlled through + thrust forces generated by individual rotors/propellers. This class provides specialized functionality + for managing multiple thruster actuators, computing combined wrenches from individual thrusts, + and applying them to the multirotor's base link. + + Key Features: + - **Thruster-based control**: Uses :class:`~isaaclab_contrib.actuators.Thruster` actuators + instead of joint actuators for realistic rotor dynamics simulation. + - **Force allocation**: Supports allocation matrices to convert individual thruster forces + into combined body wrenches (forces and torques). + - **Asymmetric dynamics**: Thruster actuators can model asymmetric rise/fall dynamics + that reflect real motor behavior. + - **Flexible configuration**: Supports arbitrary numbers and arrangements of thrusters + through regex-based thruster naming patterns. + + Usage Example: + .. code-block:: python + + import isaaclab.sim as sim_utils + from isaaclab_contrib.assets import MultirotorCfg + from isaaclab_contrib.actuators import ThrusterCfg + + # Define thruster actuator configuration + thruster_cfg = ThrusterCfg( + thruster_names_expr=["rotor_[0-3]"], # Match rotors 0-3 + thrust_range=(0.0, 10.0), # Min and max thrust in Newtons + rise_time_constant=0.1, # Time constant for thrust increase + fall_time_constant=0.2, # Time constant for thrust decrease + ) + + # Create multirotor configuration + multirotor_cfg = MultirotorCfg( + prim_path="/World/envs/env_.*/Robot", + spawn=sim_utils.UsdFileCfg(usd_path="path/to/quadcopter.usd"), + actuators={"thrusters": thruster_cfg}, + allocation_matrix=[ # 6x4 matrix for quadcopter (6 DOF, 4 thrusters) + [1.0, 1.0, 1.0, 1.0], # Total vertical force + [0.0, 0.0, 0.0, 0.0], # Lateral force (x) + [0.0, 0.0, 0.0, 0.0], # Lateral force (y) + [0.0, 0.1, 0.0, -0.1], # Roll torque + [-0.1, 0.0, 0.1, 0.0], # Pitch torque + [0.01, -0.01, 0.01, -0.01], # Yaw torque + ], + ) + + # Create the multirotor instance + multirotor = multirotor_cfg.class_type(multirotor_cfg) + + .. note:: + The allocation matrix maps individual thruster forces to a 6D wrench (3 forces + 3 torques) + applied to the base link. The matrix dimensions should be (6, num_thrusters). + + .. seealso:: + - :class:`~isaaclab.assets.Articulation`: Base articulation class + - :class:`MultirotorCfg`: Configuration class for multirotors + - :class:`MultirotorData`: Data container for multirotor state + - :class:`~isaaclab_contrib.actuators.Thruster`: Thruster actuator model + """ + + cfg: MultirotorCfg + """Configuration instance for the multirotor.""" + + actuators: dict[str, Thruster] + """Dictionary of thruster actuator instances for the multirotor. + + The keys are the actuator names and the values are the actuator instances. The actuator instances + are initialized based on the actuator configurations specified in the :attr:`MultirotorCfg.actuators` + attribute. They are used to compute the thruster commands during the :meth:`write_data_to_sim` function. + """ + + def __init__(self, cfg: MultirotorCfg): + """Initialize the multirotor articulation. + + Args: + cfg: A configuration instance. + """ + super().__init__(cfg) + + """ + Properties + """ + + @property + def thruster_names(self) -> list[str]: + """Ordered names of thrusters in the multirotor. + + This property aggregates thruster names from all thruster actuator groups configured + for the multirotor. The names are ordered according to their array indices, which is + important for setting thrust targets and interpreting thruster data. + + Returns: + A list of thruster names in order. Returns an empty list if actuators are not yet initialized. + + Raises: + ValueError: If a non-thruster actuator is found in the multirotor actuators. + """ + if not hasattr(self, "actuators") or not self.actuators: + return [] + + thruster_names = [] + for actuator in self.actuators.values(): + if hasattr(actuator, "thruster_names"): + thruster_names.extend(actuator.thruster_names) + else: + raise ValueError("Non thruster actuator found in multirotor actuators. Not supported at the moment.") + + return thruster_names + + @property + def num_thrusters(self) -> int: + """Number of thrusters in the multirotor. + + Returns: + Total number of thrusters across all actuator groups. + """ + return len(self.thruster_names) + + @property + def allocation_matrix(self) -> torch.Tensor: + """Allocation matrix for control allocation. + + The allocation matrix maps individual thruster forces to a 6D wrench vector + (3 forces + 3 torques) applied to the base link. This allows converting + per-thruster commands into the resulting body-frame forces and moments. + + The matrix has shape (6, num_thrusters), where: + - Rows 0-2: Force contributions in body frame (Fx, Fy, Fz) + - Rows 3-5: Torque contributions in body frame (Tx, Ty, Tz) + + Returns: + Allocation matrix as a torch tensor on the device. + """ + return torch.tensor(self.cfg.allocation_matrix, device=self.device, dtype=torch.float32) + + """ + Operations + """ + + def set_thrust_target( + self, + target: torch.Tensor, + thruster_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set target thrust values for thrusters. + + This method sets the desired thrust values for specific thrusters in specific environments. + The thrust targets are stored and later processed by the thruster actuator models during + the :meth:`write_data_to_sim` call. The actuator models may apply dynamics (rise/fall times) + and constraints (thrust limits) to these targets. + + Args: + target: Target thrust values. Shape is (num_envs, num_thrusters) or (num_envs,). + The values are typically in the same units as configured in the thruster actuator + (e.g., Newtons for force, or revolutions per second for RPS). + thruster_ids: Indices of thrusters to set. Defaults to None (all thrusters). + Can be a sequence of integers, a slice, or None. + env_ids: Environment indices to set. Defaults to None (all environments). + Can be a sequence of integers or None. + + Example: + .. code-block:: python + + # Set thrust for all thrusters in all environments + multirotor.set_thrust_target(torch.ones(num_envs, 4) * 5.0) + + # Set thrust for specific thrusters + multirotor.set_thrust_target( + torch.tensor([[5.0, 6.0]]), # Different thrust for 2 thrusters + thruster_ids=[0, 2], # Apply to thrusters 0 and 2 + env_ids=[0], # Only in environment 0 + ) + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if thruster_ids is None: + thruster_ids = slice(None) + + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and thruster_ids != slice(None): + env_ids = env_ids[:, None] + + # set targets + self._data.thrust_target[env_ids, thruster_ids] = target + + def reset(self, env_ids: Sequence[int] | None = None): + """Reset the multirotor to default state. + + This method resets both the base articulation state (pose, velocities) and + multirotor-specific state (thruster targets) to their default values as specified + in the configuration. + + Args: + env_ids: Environment indices to reset. Defaults to None (all environments). + Can be a sequence of integers or None. + + Note: + The default thruster state is set via the :attr:`MultirotorCfg.init_state.rps` + configuration parameter. + """ + # call parent reset + super().reset(env_ids) + + # reset multirotor-specific data + if env_ids is None: + env_ids = self._ALL_INDICES + elif not isinstance(env_ids, torch.Tensor): + env_ids = torch.tensor(env_ids, dtype=torch.long, device=self.device) + + # reset thruster targets to default values + if self._data.thrust_target is not None and self._data.default_thruster_rps is not None: + self._data.thrust_target[env_ids] = self._data.default_thruster_rps[env_ids] + + def write_data_to_sim(self): + """Write thrust and torque commands to the simulation. + + This method performs the following operations in sequence: + + 1. **Apply actuator models**: Process thrust targets through thruster actuator models + to compute actual thrust values considering dynamics (rise/fall times) and + constraints (thrust limits). + + 2. **Combine thrusts into wrench**: Use the allocation matrix to convert individual + thruster forces into a combined 6D wrench (force + torque) vector. + + 3. **Apply to simulation**: Apply the combined wrench to the base link of the multirotor + in the PhysX simulation. + + This method should be called after setting thrust targets with :meth:`set_thrust_target` + and before stepping the simulation. + + Note: + This method overrides the base class implementation because multirotors use thrust-based + control rather than joint-based control. + """ + self._apply_actuator_model() + # apply thruster forces at individual locations + self._apply_combined_wrench() + + """ + Internal methods + """ + + def _initialize_impl(self): + """Initialize the multirotor implementation.""" + # call parent initialization + super()._initialize_impl() + + # Replace data container with MultirotorData + self._data = MultirotorData(self.root_physx_view, self.device) + + # Create thruster buffers with correct size (SINGLE PHASE) + self._create_thruster_buffers() + # Process thruster configuration + self._process_thruster_cfg() + # Process configuration + self._process_cfg() + # Update the robot data + self.update(0.0) + + # Log multirotor information + self._log_multirotor_info() + + def _create_thruster_buffers(self): + """Create thruster buffers with correct size.""" + num_instances = self.num_instances + num_thrusters = self._count_thrusters_from_config() + + # Create thruster data tensors with correct size + self._data.default_thruster_rps = torch.zeros(num_instances, num_thrusters, device=self.device) + # thrust after controller and allocation is applied + self._data.thrust_target = torch.zeros(num_instances, num_thrusters, device=self.device) + self._data.computed_thrust = torch.zeros(num_instances, num_thrusters, device=self.device) + self._data.applied_thrust = torch.zeros(num_instances, num_thrusters, device=self.device) + + # Combined wrench buffers + self._thrust_target_sim = torch.zeros_like(self._data.thrust_target) # thrust after actuator model is applied + # wrench target for combined mode + self._internal_wrench_target_sim = torch.zeros(num_instances, 6, device=self.device) + # internal force/torque targets per body for combined mode + self._internal_force_target_sim = torch.zeros(num_instances, self.num_bodies, 3, device=self.device) + self._internal_torque_target_sim = torch.zeros(num_instances, self.num_bodies, 3, device=self.device) + + # Placeholder thruster names (will be filled during actuator creation) + self._data.thruster_names = [f"thruster_{i}" for i in range(num_thrusters)] + + def _count_thrusters_from_config(self) -> int: + """Count total number of thrusters from actuator configuration. + + This method parses all actuator configurations to determine the total number + of thrusters before they are initialized. It uses the thruster name expressions + to find matching bodies in the USD prim. + + Returns: + Total number of thrusters across all actuator groups. + + Raises: + ValueError: If no thrusters are found in the configuration. + """ + total_thrusters = 0 + + for actuator_name, actuator_cfg in self.cfg.actuators.items(): + if not hasattr(actuator_cfg, "thruster_names_expr"): + continue + + # Use find_bodies to count thrusters for this actuator + body_indices, thruster_names = self.find_bodies(actuator_cfg.thruster_names_expr) + total_thrusters += len(body_indices) + + if total_thrusters == 0: + raise ValueError( + "No thrusters found in actuator configuration. " + "Please check 'thruster_names_expr' in the provided 'MultirotorCfg.actuators' configuration." + ) + + return total_thrusters + + def _process_actuators_cfg(self): + """Override parent method to do nothing - we handle thrusters separately.""" + # Do nothing - we handle thruster processing in _process_thruster_cfg() otherwise this + # gives issues with joint name expressions + pass + + def _process_cfg(self): + """Post processing of multirotor configuration parameters.""" + # Handle root state (like parent does) + default_root_state = ( + tuple(self.cfg.init_state.pos) + + tuple(self.cfg.init_state.rot) + + tuple(self.cfg.init_state.lin_vel) + + tuple(self.cfg.init_state.ang_vel) + ) + default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device) + self._data.default_root_state = default_root_state.repeat(self.num_instances, 1) + + # Handle thruster-specific initial state + if hasattr(self._data, "default_thruster_rps") and hasattr(self.cfg.init_state, "rps"): + # Match against thruster names + indices_list, _, values_list = string_utils.resolve_matching_names_values( + self.cfg.init_state.rps, self.thruster_names + ) + if indices_list: + rps_values = torch.tensor(values_list, device=self.device) + self._data.default_thruster_rps[:, indices_list] = rps_values + self._data.thrust_target[:, indices_list] = rps_values + + def _process_thruster_cfg(self): + """Process and apply multirotor thruster properties.""" + # create actuators + self.actuators = dict() + self._has_implicit_actuators = False + + # Check for mixed configurations (same as before) + has_thrusters = False + has_joints = False + + for actuator_name, actuator_cfg in self.cfg.actuators.items(): + if hasattr(actuator_cfg, "thruster_names_expr"): + has_thrusters = True + elif hasattr(actuator_cfg, "joint_names_expr"): + has_joints = True + + if has_thrusters and has_joints: + raise ValueError("Mixed configurations with both thrusters and regular joints are not supported.") + + if has_joints: + raise ValueError("Regular joint actuators are not supported in Multirotor class.") + + # Store the body-to-thruster mapping + self._thruster_body_mapping = {} + + # Track thruster names as we create actuators + all_thruster_names = [] + + for actuator_name, actuator_cfg in self.cfg.actuators.items(): + body_indices, thruster_names = self.find_bodies(actuator_cfg.thruster_names_expr) + + # Create 0-based thruster array indices starting from current count + start_idx = len(all_thruster_names) + thruster_array_indices = list(range(start_idx, start_idx + len(body_indices))) + + # Track all thruster names + all_thruster_names.extend(thruster_names) + + # Store the mapping + self._thruster_body_mapping[actuator_name] = { + "body_indices": body_indices, + "array_indices": thruster_array_indices, + "thruster_names": thruster_names, + } + + # Create thruster actuator + actuator: Thruster = actuator_cfg.class_type( + cfg=actuator_cfg, + thruster_names=thruster_names, + thruster_ids=thruster_array_indices, + num_envs=self.num_instances, + device=self.device, + init_thruster_rps=self._data.default_thruster_rps[:, thruster_array_indices], + ) + + # Store actuator + self.actuators[actuator_name] = actuator + + # Log information + logger.info( + f"Thruster actuator: {actuator_name} with model '{actuator_cfg.class_type.__name__}'" + f" (thruster names: {thruster_names} [{body_indices}])." + ) + + # Update thruster names in data container + self._data.thruster_names = all_thruster_names + + # Log summary + logger.info(f"Initialized {len(self.actuators)} thruster actuator(s) for multirotor.") + + def _apply_actuator_model(self): + """Processes thruster commands for the multirotor by forwarding them to the actuators. + + This internal method iterates through all thruster actuator groups and applies their + respective actuator models to the thrust targets. The actuator models simulate realistic + motor dynamics including: + + - Rise/fall time constants for asymmetric response + - Thrust saturation and clipping to physical limits + - Integration of motor dynamics over time + + The computed thrust values are stored in internal buffers for subsequent wrench computation. + + Note: + This method updates: + - :attr:`_thrust_target_sim`: Processed thrust values after actuator model + - :attr:`_data.computed_thrust`: Thrust before saturation + - :attr:`_data.applied_thrust`: Final thrust after saturation + """ + + # process thruster actions per group + for actuator in self.actuators.values(): + if not isinstance(actuator, Thruster): + continue + + # prepare input for actuator model based on cached data + control_action = MultiRotorActions( + thrusts=self._data.thrust_target[:, actuator.thruster_indices], + thruster_indices=actuator.thruster_indices, + ) + + # compute thruster command from the actuator model + control_action = actuator.compute(control_action) + + # update targets (these are set into the simulation) + if control_action.thrusts is not None: + self._thrust_target_sim[:, actuator.thruster_indices] = control_action.thrusts + + # update state of the actuator model + self._data.computed_thrust[:, actuator.thruster_indices] = actuator.computed_thrust + self._data.applied_thrust[:, actuator.thruster_indices] = actuator.applied_thrust + + def _apply_combined_wrench(self): + """Apply combined wrench to the base link. + + This internal method applies the 6D wrench (computed by :meth:`_combine_thrusts`) + to the base link of the multirotor. The wrench is applied at the center of mass + of the base link in the local body frame. + + The forces and torques are applied through PhysX's force/torque API, which integrates + them during the physics step to produce accelerations and velocities. + """ + # Combine individual thrusts into a wrench vector + self._combine_thrusts() + + self.root_physx_view.apply_forces_and_torques_at_position( + force_data=self._internal_force_target_sim.view(-1, 3), # Shape: (num_envs * num_bodies, 3) + torque_data=self._internal_torque_target_sim.view(-1, 3), # Shape: (num_envs * num_bodies, 3) + position_data=None, # Apply at center of mass + indices=self._ALL_INDICES, + is_global=False, # Forces are in local frame + ) + + def _combine_thrusts(self): + """Combine individual thrusts into a wrench vector. + + This internal method uses the allocation matrix to convert individual thruster + forces into a 6D wrench vector (3D force + 3D torque) in the body frame. The + wrench is then assigned to the base link (body index 0) for application to + the simulation. + + The allocation matrix encodes the geometric configuration of the thrusters, + including their positions and orientations relative to the center of mass. + + Mathematical operation: + wrench = allocation_matrix @ thrusts + where wrench = [Fx, Fy, Fz, Tx, Ty, Tz]^T + """ + thrusts = self._thrust_target_sim + self._internal_wrench_target_sim = (self.allocation_matrix @ thrusts.T).T + # Apply forces to base link (body index 0) only + self._internal_force_target_sim[:, 0, :] = self._internal_wrench_target_sim[:, :3] + self._internal_torque_target_sim[:, 0, :] = self._internal_wrench_target_sim[:, 3:] + + def _validate_cfg(self): + """Validate the multirotor configuration after processing. + + Note: + This function should be called only after the configuration has been processed and the buffers have been + created. Otherwise, some settings that are altered during processing may not be validated. + """ + # Only validate if actuators have been created + if hasattr(self, "actuators") and self.actuators: + # Validate thruster-specific configuration + for actuator_name in self.actuators: + if isinstance(self.actuators[actuator_name], Thruster): + initial_thrust = self.actuators[actuator_name].curr_thrust + # check that the initial thrust is within the limits + thrust_limits = self.actuators[actuator_name].cfg.thrust_range + if torch.any(initial_thrust < thrust_limits[0]) or torch.any(initial_thrust > thrust_limits[1]): + raise ValueError( + f"Initial thrust for actuator '{actuator_name}' is out of bounds: " + f"{initial_thrust} not in {thrust_limits}" + ) + + def _log_multirotor_info(self): + """Log multirotor-specific information.""" + logger.info(f"Multirotor initialized with {self.num_thrusters} thrusters") + logger.info(f"Thruster names: {self.thruster_names}") + logger.info(f"Thruster force direction: {self.cfg.thruster_force_direction}") diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_cfg.py new file mode 100644 index 000000000000..9638fcf2aa66 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_cfg.py @@ -0,0 +1,286 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from collections.abc import Sequence +from dataclasses import MISSING + +from isaaclab.assets.articulation import ArticulationCfg +from isaaclab.utils import configclass + +from isaaclab_contrib.actuators import ThrusterCfg + +from .multirotor import Multirotor + + +@configclass +class MultirotorCfg(ArticulationCfg): + """Configuration parameters for a multirotor articulation. + + This configuration class extends :class:`~isaaclab.assets.ArticulationCfg` to add + multirotor-specific parameters including thruster actuators, allocation matrices, + and thruster-specific initial states. + + Unlike standard articulations that use joint actuators, multirotors are configured + with :class:`~isaaclab_contrib.actuators.ThrusterCfg` actuators that model individual + rotor/propeller dynamics. + + Key Configuration Parameters: + - **actuators**: Dictionary mapping actuator names to :class:`~isaaclab_contrib.actuators.ThrusterCfg` + configurations. Each configuration defines a group of thrusters with shared properties. + - **allocation_matrix**: Maps individual thruster forces to 6D body wrenches. This matrix + encodes the geometric configuration and should have shape (6, num_thrusters). + - **thruster_force_direction**: Direction vector in body frame that thrusters push along. + Typically (0, 0, 1) for upward-facing thrusters. + - **rotor_directions**: Spin direction of each rotor (1 for CCW, -1 for CW). Used for + computing reaction torques. + + Example: + .. code-block:: python + + from isaaclab_contrib.assets import MultirotorCfg + from isaaclab_contrib.actuators import ThrusterCfg + import isaaclab.sim as sim_utils + + # Quadcopter configuration + quadcopter_cfg = MultirotorCfg( + prim_path="/World/envs/env_.*/Quadcopter", + spawn=sim_utils.UsdFileCfg( + usd_path="path/to/quadcopter.usd", + ), + init_state=MultirotorCfg.InitialStateCfg( + pos=(0.0, 0.0, 1.0), # Start 1m above ground + rps={".*": 110.0}, # All thrusters at 110 RPS (hover) + ), + actuators={ + "thrusters": ThrusterCfg( + thruster_names_expr=["rotor_[0-3]"], + thrust_range=(0.0, 12.0), # 0-12N per thruster + rise_time_constant=0.12, + fall_time_constant=0.25, + ), + }, + allocation_matrix=[ + [1.0, 1.0, 1.0, 1.0], # Vertical thrust + [0.0, 0.0, 0.0, 0.0], # Lateral force X + [0.0, 0.0, 0.0, 0.0], # Lateral force Y + [0.0, 0.13, 0.0, -0.13], # Roll torque + [-0.13, 0.0, 0.13, 0.0], # Pitch torque + [0.01, -0.01, 0.01, -0.01], # Yaw torque + ], + rotor_directions=[1, -1, 1, -1], # Alternating CW/CCW + ) + + .. seealso:: + - :class:`~isaaclab.assets.ArticulationCfg`: Base articulation configuration + - :class:`~isaaclab_contrib.actuators.ThrusterCfg`: Thruster actuator configuration + - :class:`Multirotor`: Multirotor asset class + """ + + class_type: type = Multirotor + + @configclass + class InitialStateCfg(ArticulationCfg.InitialStateCfg): + """Initial state of the multirotor articulation. + + This extends the base articulation initial state to include thruster-specific + initial conditions. The thruster initial state is particularly important for + multirotor stability, as it determines the starting thrust levels. + + For hovering multirotors, the initial RPS should be set to values that produce + enough thrust to counteract gravity. + """ + + # multirotor-specific initial state + rps: dict[str, float] = {".*": 100.0} + """Revolutions per second (RPS) of the thrusters. Default is 100 RPS. + + This can be specified as: + + - A dictionary mapping regex patterns to RPS values + - A single wildcard pattern like ``{".*": 100.0}`` for uniform RPS + - Explicit per-thruster values like ``{"rotor_0": 95.0, "rotor_1": 105.0}`` + + The RPS values are used to initialize the thruster states and determine the + default thrust targets when the multirotor is reset. + + Example: + .. code-block:: python + + # Uniform RPS for all thrusters + rps = {".*": 110.0} + + # Different RPS for different thruster groups + rps = {"rotor_[0-1]": 105.0, "rotor_[2-3]": 115.0} + + Note: + The actual thrust produced depends on the thruster model's thrust curve + and other parameters in :class:`~isaaclab_contrib.actuators.ThrusterCfg`. + """ + + # multirotor-specific configuration + init_state: InitialStateCfg = InitialStateCfg() + """Initial state of the multirotor object. + + This includes both the base articulation state (position, orientation, velocities) + and multirotor-specific state (thruster RPS). See :class:`InitialStateCfg` for details. + """ + + actuators: dict[str, ThrusterCfg] = MISSING + """Thruster actuators for the multirotor with corresponding thruster names. + + This dictionary maps actuator group names to their configurations. Each + :class:`~isaaclab_contrib.actuators.ThrusterCfg` defines a group of thrusters + with shared dynamic properties (rise/fall times, thrust limits, etc.). + + Example: + .. code-block:: python + + actuators = { + "thrusters": ThrusterCfg( + thruster_names_expr=["rotor_.*"], # Regex to match thruster bodies + thrust_range=(0.0, 10.0), + rise_time_constant=0.1, + fall_time_constant=0.2, + ) + } + + Note: + Unlike standard articulations, multirotors should only contain thruster actuators. + Mixing joint-based and thrust-based actuators is not currently supported. + """ + + # multirotor force application settings + thruster_force_direction: tuple[float, float, float] = (0.0, 0.0, 1.0) + """Default force direction in body-local frame for thrusters. Default is ``(0.0, 0.0, 1.0)``, + which is upward along the Z-axis. + + This 3D unit vector specifies the direction in which thrusters generate force + in the multirotor's body frame. For standard configurations: + + - ``(0.0, 0.0, 1.0)``: Thrusters push upward (Z-axis, typical for quadcopters) + - ``(0.0, 0.0, -1.0)``: Thrusters push downward + - ``(1.0, 0.0, 0.0)``: Thrusters push forward (X-axis) + + This is used in conjunction with the allocation matrix to compute the wrench + produced by each thruster. + + Default: ``(0.0, 0.0, 1.0)`` (upward along Z-axis) + """ + + allocation_matrix: Sequence[Sequence[float]] | None = None + """Allocation matrix for control allocation. Default is ``None``, which means that the thrusters + are not used for control allocation. + + This matrix maps individual thruster forces to the 6D wrench (force + torque) + applied to the multirotor's base link. It has shape ``(6, num_thrusters)``: + + - **Rows 0-2**: Force contributions in body frame (Fx, Fy, Fz) + - **Rows 3-5**: Torque contributions in body frame (Tx, Ty, Tz) + + The allocation matrix encodes the geometric configuration of the multirotor, + including thruster positions, orientations, and moment arms. + + Example for a quadcopter (4 thrusters in + configuration): + .. code-block:: python + + allocation_matrix = [ + [1.0, 1.0, 1.0, 1.0], # Total vertical thrust + [0.0, 0.0, 0.0, 0.0], # No lateral force + [0.0, 0.0, 0.0, 0.0], # No lateral force + [0.0, 0.13, 0.0, -0.13], # Roll moment (left/right) + [-0.13, 0.0, 0.13, 0.0], # Pitch moment (forward/back) + [0.01,-0.01, 0.01,-0.01], # Yaw moment (rotation) + ] + + Note: + If ``None``, forces must be applied through other means. For typical + multirotor control, this should always be specified. + """ + + rotor_directions: Sequence[int] | None = None + """Sequence of rotor directions for each thruster. Default is ``None``, which means that the rotor directions + are not specified. + + This specifies the spin direction of each rotor, which affects the reaction + torques generated. Values should be: + + - ``1``: Counter-clockwise (CCW) rotation + - ``-1``: Clockwise (CW) rotation + + For a quadcopter, a typical configuration is alternating directions to + cancel reaction torques during hover: ``[1, -1, 1, -1]``. + + Example: + .. code-block:: python + + # Quadcopter with alternating rotor directions + rotor_directions = [1, -1, 1, -1] + + # Hexacopter + rotor_directions = [1, -1, 1, -1, 1, -1] + + Note: + The length must match the total number of thrusters defined in the + actuators configuration, otherwise a ``ValueError`` will be raised + during initialization. + """ + + def __post_init__(self): + """Post initialization validation.""" + # Skip validation if actuators is MISSING + if self.actuators is MISSING: + return + + # Count the total number of thrusters from all actuator configs + num_thrusters = 0 + for thruster_cfg in self.actuators.values(): + if hasattr(thruster_cfg, "thruster_names_expr") and thruster_cfg.thruster_names_expr is not None: + num_thrusters += len(thruster_cfg.thruster_names_expr) + + # Validate rotor_directions matches number of thrusters + if self.rotor_directions is not None: + num_rotor_directions = len(self.rotor_directions) + if num_thrusters != num_rotor_directions: + raise ValueError( + f"Mismatch between number of thrusters ({num_thrusters}) and " + f"rotor_directions ({num_rotor_directions}). " + "They must have the same number of elements." + ) + + # Validate rps explicit entries match number of thrusters + # Only validate if rps has explicit entries (not just a wildcard pattern) + if hasattr(self.init_state, "rps") and self.init_state.rps is not None: + rps_keys = list(self.init_state.rps.keys()) + # Check if rps uses a wildcard pattern (single key that's a regex) + is_wildcard = len(rps_keys) == 1 and (rps_keys[0] == ".*" or rps_keys[0] == ".*:.*") + + if not is_wildcard and len(rps_keys) != num_thrusters: + raise ValueError( + f"Mismatch between number of thrusters ({num_thrusters}) and " + f"rps entries ({len(rps_keys)}). " + "They must have the same number of elements when using explicit rps keys." + ) + + # Validate allocation_matrix second dimension matches number of thrusters + if self.allocation_matrix is not None: + if len(self.allocation_matrix) == 0: + raise ValueError("Allocation matrix cannot be empty.") + + # Check that all rows have the same length + num_cols = len(self.allocation_matrix[0]) + for i, row in enumerate(self.allocation_matrix): + if len(row) != num_cols: + raise ValueError( + f"Allocation matrix row {i} has length {len(row)}, " + f"but expected {num_cols} (all rows must have the same length)." + ) + + # Validate that the second dimension (columns) matches number of thrusters + if num_cols != num_thrusters: + raise ValueError( + f"Mismatch between number of thrusters ({num_thrusters}) and " + f"allocation matrix columns ({num_cols}). " + "The second dimension of the allocation matrix must match the number of thrusters." + ) diff --git a/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_data.py b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_data.py new file mode 100644 index 000000000000..05ea56c4565a --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/assets/multirotor/multirotor_data.py @@ -0,0 +1,105 @@ +# 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 torch + +from isaaclab.assets.articulation.articulation_data import ArticulationData + + +class MultirotorData(ArticulationData): + """Data container for a multirotor articulation. + + This class extends the base :class:`~isaaclab.assets.ArticulationData` container to include + multirotor-specific data such as thruster states, thrust targets, and computed forces. + It provides access to all the state information needed for monitoring and controlling + multirotor vehicles. + + The data container is automatically created and managed by the :class:`~isaaclab_contrib.assets.Multirotor` + class. Users typically access this data through the :attr:`Multirotor.data` property. + + Note: + All tensor attributes have shape ``(num_instances, num_thrusters)`` where + ``num_instances`` is the number of environment instances and ``num_thrusters`` + is the total number of thrusters per multirotor. + + .. seealso:: + - :class:`~isaaclab.assets.ArticulationData`: Base articulation data container + - :class:`~isaaclab_contrib.assets.Multirotor`: Multirotor asset class + """ + + thruster_names: list[str] = None + """List of thruster names in the multirotor. + + This list contains the ordered names of all thrusters, matching the order used + for indexing in the thrust tensors. The names correspond to the USD body prim names + matched by the thruster name expressions in the actuator configuration. + + Example: + ``["rotor_0", "rotor_1", "rotor_2", "rotor_3"]`` for a quadcopter + """ + + default_thruster_rps: torch.Tensor = None + """Default thruster RPS (revolutions per second) state of all thrusters. Shape is (num_instances, num_thrusters). + + This quantity is configured through the :attr:`MultirotorCfg.init_state.rps` parameter + and represents the baseline/hover RPS for each thruster. It is used to initialize + thruster states during reset operations. + + For a hovering multirotor, these values should produce enough collective thrust + to counteract gravity. + + Example: + For a 1kg quadcopter with 4 thrusters, if each thruster produces 2.5N at 110 RPS, + the default might be ``[[110.0, 110.0, 110.0, 110.0]]`` for hover. + """ + + thrust_target: torch.Tensor = None + """Thrust targets commanded by the user or controller. Shape is ``(num_instances, num_thrusters)`` + + This quantity contains the target thrust values set through the + :meth:`~isaaclab_contrib.assets.Multirotor.set_thrust_target` method or by + action terms in RL environments. These targets are processed by the thruster + actuator models to compute actual applied thrusts. + + The units depend on the actuator model configuration (typically Newtons for + force or RPS for rotational speed). + """ + + ## + # Thruster commands + ## + + computed_thrust: torch.Tensor = None + """Computed thrust from the actuator model before clipping. Shape is (num_instances, num_thrusters). + + This quantity contains the thrust values computed by the thruster actuator models + before any clipping or saturation is applied. It represents the "desired" thrust + based on the actuator dynamics (rise/fall times) but may exceed physical limits. + + The difference between :attr:`computed_thrust` and :attr:`applied_thrust` indicates + when the actuator is saturating at its limits. + + Example Use: + Monitor actuator saturation by comparing computed vs applied thrust: + + .. code-block:: python + + saturation = multirotor.data.computed_thrust - multirotor.data.applied_thrust + is_saturated = saturation.abs() > 1e-6 + """ + + applied_thrust: torch.Tensor = None + """Applied thrust from the actuator model after clipping. Shape is (num_instances, num_thrusters). + + This quantity contains the final thrust values that are actually applied to the + simulation after all actuator model processing, including: + + - Dynamic response (rise/fall time constants) + - Clipping to thrust range limits + - Any other actuator model constraints + + This is the "ground truth" thrust that affects the multirotor's motion in the + physics simulation. + """ diff --git a/source/isaaclab_contrib/isaaclab_contrib/mdp/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/mdp/__init__.py new file mode 100644 index 000000000000..bc099b36f648 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/mdp/__init__.py @@ -0,0 +1,8 @@ +# 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 + +"""Sub-package for MDP (Markov Decision Process) components contributed by the community.""" + +from .actions import * # noqa: F401, F403 diff --git a/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/__init__.py new file mode 100644 index 000000000000..695a4486066f --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/__init__.py @@ -0,0 +1,14 @@ +# 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 + +"""Action terms for multirotor control. + +This module provides action terms specifically designed for controlling multirotor +vehicles through thrust commands. These action terms integrate with Isaac Lab's +MDP framework and :class:`~isaaclab_contrib.assets.Multirotor` assets. +""" + +from .thrust_actions import * # noqa: F401, F403 +from .thrust_actions_cfg import * # noqa: F401, F403 diff --git a/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/thrust_actions.py b/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/thrust_actions.py new file mode 100644 index 000000000000..7aa207849de6 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/thrust_actions.py @@ -0,0 +1,246 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.string as string_utils +from isaaclab.managers.action_manager import ActionTerm + +from isaaclab_contrib.assets import Multirotor + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + from isaaclab.envs.utils.io_descriptors import GenericActionIODescriptor + + from . import thrust_actions_cfg + +# import logger +logger = logging.getLogger(__name__) + + +class ThrustAction(ActionTerm): + """Thrust action term that applies the processed actions as thrust commands. + + This action term is designed specifically for controlling multirotor vehicles by mapping + action inputs to thruster commands. It provides flexible preprocessing of actions through: + + - **Scaling**: Multiply actions by a scale factor to adjust command magnitudes + - **Offset**: Add an offset to center actions around a baseline (e.g., hover thrust) + - **Clipping**: Constrain actions to valid ranges to prevent unsafe commands + + The action term integrates with Isaac Lab's :class:`~isaaclab.managers.ActionManager` + framework and is specifically designed to work with :class:`~isaaclab_contrib.assets.Multirotor` + assets. + + Key Features: + - Supports per-thruster or uniform scaling and offsets + - Optional automatic offset computation based on hover thrust + - Action clipping for safety and constraint enforcement + - Regex-based thruster selection for flexible control schemes + + Example: + .. code-block:: python + + from isaaclab.envs import ManagerBasedRLEnvCfg + from isaaclab_contrib.mdp.actions import ThrustActionCfg + + + @configclass + class MyEnvCfg(ManagerBasedRLEnvCfg): + # ... other configuration ... + + @configclass + class ActionsCfg: + # Direct thrust control (normalized actions) + thrust = ThrustActionCfg( + asset_name="robot", + scale=5.0, # Convert [-1, 1] to [-5, 5] N + use_default_offset=True, # Add hover thrust as offset + clip={".*": (-2.0, 8.0)}, # Clip to safe thrust range + ) + + """ + + cfg: thrust_actions_cfg.ThrustActionCfg + """The configuration of the action term.""" + _asset: Multirotor + """The articulation asset on which the action term is applied.""" + _scale: torch.Tensor | float + """The scaling factor applied to the input action.""" + _offset: torch.Tensor | float + """The offset applied to the input action.""" + _clip: torch.Tensor + """The clip applied to the input action.""" + + def __init__(self, cfg: thrust_actions_cfg.ThrustActionCfg, env: ManagerBasedEnv) -> None: + # initialize the action term + super().__init__(cfg, env) + + thruster_names_expr = self._asset.actuators["thrusters"].cfg.thruster_names_expr + + # resolve the thrusters over which the action term is applied + self._thruster_ids, self._thruster_names = self._asset.find_bodies( + thruster_names_expr, preserve_order=self.cfg.preserve_order + ) + self._num_thrusters = len(self._thruster_ids) + # log the resolved thruster names for debugging + logger.info( + f"Resolved thruster names for the action term {self.__class__.__name__}:" + f" {self._thruster_names} [{self._thruster_ids}]" + ) + + # Avoid indexing across all thrusters for efficiency + if self._num_thrusters == self._asset.num_thrusters and not self.cfg.preserve_order: + self._thruster_ids = slice(None) + + # create tensors for raw and processed actions + self._raw_actions = torch.zeros(self.num_envs, self.action_dim, device=self.device) + self._processed_actions = torch.zeros_like(self.raw_actions) + + # parse scale + if isinstance(cfg.scale, (float, int)): + self._scale = float(cfg.scale) + elif isinstance(cfg.scale, dict): + self._scale = torch.ones(self.num_envs, self.action_dim, device=self.device) + # resolve the dictionary config + index_list, _, value_list = string_utils.resolve_matching_names_values(self.cfg.scale, self._thruster_names) + self._scale[:, index_list] = torch.tensor(value_list, device=self.device) + else: + raise ValueError(f"Unsupported scale type: {type(cfg.scale)}. Supported types are float and dict.") + + # parse offset + if isinstance(cfg.offset, (float, int)): + self._offset = float(cfg.offset) + elif isinstance(cfg.offset, dict): + self._offset = torch.zeros_like(self._raw_actions) + # resolve the dictionary config + index_list, _, value_list = string_utils.resolve_matching_names_values( + self.cfg.offset, self._thruster_names + ) + self._offset[:, index_list] = torch.tensor(value_list, device=self.device) + else: + raise ValueError(f"Unsupported offset type: {type(cfg.offset)}. Supported types are float and dict.") + + # parse clip + if cfg.clip is not None: + if isinstance(cfg.clip, dict): + self._clip = torch.tensor([[-float("inf"), float("inf")]], device=self.device).repeat( + self.num_envs, self.action_dim, 1 + ) + index_list, _, value_list = string_utils.resolve_matching_names_values( + self.cfg.clip, self._thruster_names + ) + self._clip[:, index_list] = torch.tensor(value_list, device=self.device) + else: + raise ValueError(f"Unsupported clip type: {type(cfg.clip)}. Supported types are dict.") + + # Handle use_default_offset + if cfg.use_default_offset: + # Use default thruster RPS as offset + self._offset = self._asset.data.default_thruster_rps[:, self._thruster_ids].clone() + + """ + Properties + """ + + @property + def action_dim(self) -> int: + return self._num_thrusters + + @property + def raw_actions(self) -> torch.Tensor: + return self._raw_actions + + @property + def processed_actions(self) -> torch.Tensor: + return self._processed_actions + + @property + def IO_descriptor(self) -> GenericActionIODescriptor: + """The IO descriptor of the action term.""" + super().IO_descriptor + self._IO_descriptor.shape = (self.action_dim,) + self._IO_descriptor.dtype = str(self.raw_actions.dtype) + self._IO_descriptor.action_type = "ThrustAction" + self._IO_descriptor.thruster_names = self._thruster_names + self._IO_descriptor.scale = self._scale + if isinstance(self._offset, torch.Tensor): + self._IO_descriptor.offset = self._offset[0].detach().cpu().numpy().tolist() + else: + self._IO_descriptor.offset = self._offset + if self.cfg.clip is not None: + if isinstance(self._clip, torch.Tensor): + self._IO_descriptor.clip = self._clip[0].detach().cpu().numpy().tolist() + else: + self._IO_descriptor.clip = self._clip + else: + self._IO_descriptor.clip = None + return self._IO_descriptor + + """ + Methods + """ + + def reset(self, env_ids: Sequence[int] | None = None) -> None: + """Reset the action term. + + This method resets the raw actions to zero for the specified environments. + The processed actions will be recomputed during the next :meth:`process_actions` call. + + Args: + env_ids: Environment indices to reset. Defaults to None (all environments). + """ + self._raw_actions[env_ids] = 0.0 + + def process_actions(self, actions: torch.Tensor): + r"""Process actions by applying scaling, offset, and clipping. + + This method transforms raw policy actions into thrust commands through + an affine transformation followed by optional clipping. The transformation is: + + .. math:: + \text{processed} = \text{raw} \times \text{scale} + \text{offset} + + If clipping is configured, the processed actions are then clamped: + + .. math:: + \text{processed} = \text{clamp}(\text{processed}, \text{min}, \text{max}) + + Args: + actions: Raw action tensor from the policy. Shape is ``(num_envs, action_dim)``. + Typically in the range [-1, 1] for normalized policies. + + Note: + The processed actions are stored internally and applied during the next + :meth:`apply_actions` call. + """ + # store the raw actions + self._raw_actions[:] = actions + # apply the affine transformations + self._processed_actions = self._raw_actions * self._scale + self._offset + # clip actions + if self.cfg.clip is not None: + self._processed_actions = torch.clamp( + self._processed_actions, min=self._clip[:, :, 0], max=self._clip[:, :, 1] + ) + + def apply_actions(self): + """Apply the processed actions as thrust commands. + + This method sets the processed actions as thrust targets on the multirotor + asset. The thrust targets are then used by the thruster actuator models + to compute actual thrust forces during the simulation step. + + The method calls :meth:`~isaaclab_contrib.assets.Multirotor.set_thrust_target` + on the multirotor asset with the appropriate thruster IDs. + """ + # Set thrust targets using thruster IDs + self._asset.set_thrust_target(self.processed_actions, thruster_ids=self._thruster_ids) diff --git a/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/thrust_actions_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/thrust_actions_cfg.py new file mode 100644 index 000000000000..0f457fe4a5a0 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/mdp/actions/thrust_actions_cfg.py @@ -0,0 +1,168 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from isaaclab.managers.action_manager import ActionTerm, ActionTermCfg +from isaaclab.utils import configclass + +from . import thrust_actions + + +@configclass +class ThrustActionCfg(ActionTermCfg): + """Configuration for the thrust action term. + + This configuration class specifies how policy actions are transformed into thruster + commands for multirotor control. It provides extensive customization of the action + processing pipeline including scaling, offsetting, and clipping. + + The action term is designed to work with :class:`~isaaclab_contrib.assets.Multirotor` + assets and uses their thruster configuration to determine which thrusters to control. + + Key Configuration Options: + - **scale**: Multiplies raw actions to adjust command magnitude + - **offset**: Adds a baseline value (e.g., hover thrust) to actions + - **clip**: Constrains actions to safe operational ranges + - **use_default_offset**: Automatically uses hover thrust as offset + + Example Configurations: + **Normalized thrust control around hover**: + + .. code-block:: python + + thrust_action = ThrustActionCfg( + asset_name="robot", + scale=2.0, # Actions in [-1,1] become [-2,2] N + use_default_offset=True, # Add hover thrust (e.g., 5N) + clip={".*": (0.0, 10.0)}, # Final thrust in [0, 10] N + ) + + **Direct thrust control with per-thruster scaling**: + + .. code-block:: python + + thrust_action = ThrustActionCfg( + asset_name="robot", + scale={ + "rotor_[0-1]": 8.0, # Front rotors: stronger + "rotor_[2-3]": 7.0, # Rear rotors: weaker + }, + offset=0.0, + use_default_offset=False, + ) + + **Differential thrust control**: + + .. code-block:: python + + thrust_action = ThrustActionCfg( + asset_name="robot", + scale=3.0, + use_default_offset=True, # Center around hover + clip={".*": (-2.0, 8.0)}, # Allow +/-2N deviation + ) + + .. seealso:: + - :class:`~isaaclab_contrib.mdp.actions.ThrustAction`: Implementation of this action term + - :class:`~isaaclab.managers.ActionTermCfg`: Base action term configuration + """ + + class_type: type[ActionTerm] = thrust_actions.ThrustAction + + asset_name: str = MISSING + """Name or regex expression of the asset that the action will be mapped to. + + This should match the name given to the multirotor asset in the scene configuration. + For example, if the robot is defined as ``robot = MultirotorCfg(...)``, then + ``asset_name`` should be ``"robot"``. + """ + + scale: float | dict[str, float] = 1.0 + """Scale factor for the action. Default is ``1.0``, which means no scaling. + + This multiplies the raw action values to adjust the command magnitude. It can be: + + - A float: uniform scaling for all thrusters (e.g., ``2.0``) + - A dict: per-thruster scaling using regex patterns (e.g., ``{"rotor_.*": 2.5}``) + + For normalized actions in [-1, 1], the scale determines the maximum deviation + from the offset value. + + Example: + .. code-block:: python + + # Uniform scaling + scale = 5.0 # Actions of ±1 become ±5N + + # Per-thruster scaling + scale = { + "rotor_[0-1]": 8.0, # Front rotors + "rotor_[2-3]": 6.0, # Rear rotors + } + """ + + offset: float | dict[str, float] = 0.0 + """Offset factor for the action. Default is ``0.0``, which means no offset. + + This value is added to the scaled actions to establish a baseline thrust. + It can be: + + - A float: uniform offset for all thrusters (e.g., ``5.0`` for 5N hover thrust) + - A dict: per-thruster offset using regex patterns + + If :attr:`use_default_offset` is ``True``, this value is overwritten by the + default thruster RPS from the multirotor configuration. + + Example: + .. code-block:: python + + # Uniform offset (5N baseline thrust) + offset = 5.0 + + # Per-thruster offset + offset = { + "rotor_0": 5.2, + "rotor_1": 4.8, + } + """ + + clip: dict[str, tuple[float, float]] | None = None + """Clipping ranges for processed actions. Default is ``None``, which means no clipping. + + This constrains the final thrust commands to safe operational ranges after + scaling and offset are applied. It must be specified as a dictionary mapping + regex patterns to (min, max) tuples. + + Example: + .. code-block:: python + + # Clip all thrusters to [0, 10] N + clip = {".*": (0.0, 10.0)} + + # Different limits for different thrusters + clip = { + "rotor_[0-1]": (0.0, 12.0), # Front rotors + "rotor_[2-3]": (0.0, 8.0), # Rear rotors + } + + """ + + preserve_order: bool = False + """Whether to preserve the order of the asset names in the action output. Default is ``False``. + + If ``True``, the thruster ordering matches the regex pattern order exactly. + If ``False``, ordering is determined by the USD scene traversal order. + """ + + use_default_offset: bool = True + """Whether to use default thrust configured in the multirotor asset as offset. Default is ``True``. + + If ``True``, the :attr:`offset` value is overwritten with the default thruster + RPS values from :attr:`MultirotorCfg.init_state.rps`. This is useful for + controlling thrust as deviations from the hover state. + + If ``False``, the manually specified :attr:`offset` value is used. + """ diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/__init__.py new file mode 100644 index 000000000000..a7ea884318a3 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/__init__.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 + +"""Sub-package for externally contributed sensors. + +This package provides specialized sensor classes for simulating externally contributed +sensors in Isaac Lab. These sensors are not part of the core Isaac Lab framework yet, +but are planned to be added in the future. They are contributed by the community to +extend the capabilities of Isaac Lab. + +Following the categorization in :mod:`isaaclab.sensors` sub-package, the prim paths passed +to the sensor's configuration class are interpreted differently based on the sensor type. +The following table summarizes the interpretation of the prim paths for different sensor types: + ++---------------------+---------------------------+---------------------------------------------------------------+ +| Sensor Type | Example Prim Path | Pre-check | ++=====================+===========================+===============================================================+ +| Visuo-Tactile Sensor| /World/robot/base | Leaf exists and is a physics body (Rigid Body) | ++---------------------+---------------------------+---------------------------------------------------------------+ + +""" + +from .tacsl_sensor import * diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/__init__.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/__init__.py new file mode 100644 index 000000000000..869b233d166d --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/__init__.py @@ -0,0 +1,10 @@ +# 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 + +"""TacSL Tactile Sensor implementation for IsaacLab.""" + +from .visuotactile_sensor import VisuoTactileSensor +from .visuotactile_sensor_cfg import GelSightRenderCfg, VisuoTactileSensorCfg +from .visuotactile_sensor_data import VisuoTactileSensorData diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_render.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_render.py new file mode 100644 index 000000000000..27d21d03736c --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_render.py @@ -0,0 +1,293 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import logging +import os +from typing import TYPE_CHECKING + +import cv2 +import numpy as np +import scipy +import torch + +from isaaclab.utils.assets import retrieve_file_path + +logger = logging.getLogger(__name__) + + +if TYPE_CHECKING: + from .visuotactile_sensor_cfg import GelSightRenderCfg + + +def compute_tactile_shear_image( + tactile_normal_force: np.ndarray, + tactile_shear_force: np.ndarray, + normal_force_threshold: float = 0.00008, + shear_force_threshold: float = 0.0005, + resolution: int = 30, +) -> np.ndarray: + """Visualize the tactile shear field. + + This function creates a visualization of tactile forces using arrows to represent shear forces + and color coding to represent normal forces. The thresholds are used to normalize forces for + visualization, chosen empirically to provide clear visual representation. + + Args: + tactile_normal_force: Array of tactile normal forces. Shape: (H, W). + tactile_shear_force: Array of tactile shear forces. Shape: (H, W, 2). + normal_force_threshold: Threshold for normal force visualization. Defaults to 0.00008. + shear_force_threshold: Threshold for shear force visualization. Defaults to 0.0005. + resolution: Resolution for the visualization. Defaults to 30. + + Returns: + Image visualizing the tactile shear forces. Shape: (H * resolution, W * resolution, 3). + """ + nrows = tactile_normal_force.shape[0] + ncols = tactile_normal_force.shape[1] + + imgs_tactile = np.zeros((nrows * resolution, ncols * resolution, 3), dtype=float) + + for row in range(nrows): + for col in range(ncols): + loc0_x = row * resolution + resolution // 2 + loc0_y = col * resolution + resolution // 2 + loc1_x = loc0_x + tactile_shear_force[row, col][0] / shear_force_threshold * resolution + loc1_y = loc0_y + tactile_shear_force[row, col][1] / shear_force_threshold * resolution + color = ( + 0.0, + max(0.0, 1.0 - tactile_normal_force[row][col] / normal_force_threshold), + min(1.0, tactile_normal_force[row][col] / normal_force_threshold), + ) + + cv2.arrowedLine( + imgs_tactile, (int(loc0_y), int(loc0_x)), (int(loc1_y), int(loc1_x)), color, 6, tipLength=0.4 + ) + + return imgs_tactile + + +def compute_penetration_depth( + penetration_depth_img: np.ndarray, resolution: int = 5, depth_multiplier: float = 300.0 +) -> np.ndarray: + """Visualize the penetration depth. + + Args: + penetration_depth_img: Image of penetration depth. Shape: (H, W). + resolution: Resolution for the upsampling; each pixel expands to a (res x res) block. Defaults to 5. + depth_multiplier: Multiplier for the depth values. Defaults to 300.0 (scales ~3.3mm to 1.0). + (e.g. typical Gelsight sensors have maximum penetration depths < 2.5mm, + see https://dspace.mit.edu/handle/1721.1/114627). + + Returns: + Upsampled image visualizing the penetration depth. Shape: (H * resolution, W * resolution). + """ + # penetration_depth_img_upsampled = penetration_depth.repeat(resolution, 0).repeat(resolution, 1) + penetration_depth_img_upsampled = np.kron(penetration_depth_img, np.ones((resolution, resolution))) + penetration_depth_img_upsampled = np.clip(penetration_depth_img_upsampled, 0.0, 1.0) * depth_multiplier + return penetration_depth_img_upsampled + + +class GelsightRender: + """Class to handle GelSight rendering using the Taxim example-based approach from :cite:t:`si2022taxim`. + + Reference: + Si, Z., & Yuan, W. (2022). Taxim: An example-based simulation model for GelSight + tactile sensors. IEEE Robotics and Automation Letters, 7(2), 2361-2368. + https://arxiv.org/abs/2109.04027 + """ + + def __init__(self, cfg: GelSightRenderCfg, device: str | torch.device): + """Initialize the GelSight renderer. + + Args: + cfg: Configuration object for the GelSight sensor. + device: Device to use ('cpu' or 'cuda'). + + Raises: + ValueError: If :attr:`GelSightRenderCfg.mm_per_pixel` is zero or negative. + FileNotFoundError: If render data files cannot be retrieved. + """ + self.cfg = cfg + self.device = device + + # Validate configuration parameters + eps = 1e-9 + if self.cfg.mm_per_pixel < eps: + raise ValueError(f"Input 'mm_per_pixel' must be positive (>= {eps}), got {self.cfg.mm_per_pixel}") + + # Retrieve render data files using the configured base path + bg_path = self._get_render_data(self.cfg.sensor_data_dir_name, self.cfg.background_path) + calib_path = self._get_render_data(self.cfg.sensor_data_dir_name, self.cfg.calib_path) + + if bg_path is None or calib_path is None: + raise FileNotFoundError( + "Failed to retrieve GelSight render data files. " + f"Base path: {self.cfg.base_data_path or 'default (Isaac Lab Nucleus)'}, " + f"Data dir: {self.cfg.sensor_data_dir_name}" + ) + + self.background = cv2.cvtColor(cv2.imread(bg_path), cv2.COLOR_BGR2RGB) + + # Load calibration data directly + calib_data = np.load(calib_path) + calib_grad_r = calib_data["grad_r"] + calib_grad_g = calib_data["grad_g"] + calib_grad_b = calib_data["grad_b"] + + image_height = self.cfg.image_height + image_width = self.cfg.image_width + num_bins = self.cfg.num_bins + [xx, yy] = np.meshgrid(range(image_width), range(image_height)) + xf = xx.flatten() + yf = yy.flatten() + self.A = np.array([xf * xf, yf * yf, xf * yf, xf, yf, np.ones(image_height * image_width)]).T + + binm = num_bins - 1 + self.x_binr = 0.5 * np.pi / binm # x [0,pi/2] + self.y_binr = 2 * np.pi / binm # y [-pi, pi] + + kernel = self._get_filtering_kernel(kernel_size=5) + self.kernel = torch.tensor(kernel, dtype=torch.float, device=self.device) + + self.calib_data_grad_r = torch.tensor(calib_grad_r, device=self.device) + self.calib_data_grad_g = torch.tensor(calib_grad_g, device=self.device) + self.calib_data_grad_b = torch.tensor(calib_grad_b, device=self.device) + + self.A_tensor = torch.tensor(self.A.reshape(image_height, image_width, 6), device=self.device).unsqueeze(0) + self.background_tensor = torch.tensor(self.background, device=self.device) + + # Pre-allocate buffer for RGB output (will be resized if needed) + self._sim_img_rgb_buffer = torch.empty((1, image_height, image_width, 3), device=self.device) + + logger.info("Gelsight renderer initialization done!") + + def render(self, height_map: torch.Tensor) -> torch.Tensor: + """Render the height map using the GelSight sensor. + + Args: + height_map: Input height map tensor. Shape is (N, H, W). + + Returns: + Rendered image tensor. Shape is (N, H, W, 3). + """ + height_map = height_map.clone() + height_map[torch.abs(height_map) < 1e-6] = 0 # remove minor artifact + height_map = height_map * -1000.0 + height_map /= self.cfg.mm_per_pixel + + height_map = self._gaussian_filtering(height_map.unsqueeze(-1), self.kernel).squeeze(-1) + + grad_mag, grad_dir = self._generate_normals(height_map) + + idx_x = torch.floor(grad_mag / self.x_binr).long() + idx_y = torch.floor((grad_dir + np.pi) / self.y_binr).long() + + # Clamp indices to valid range to prevent out-of-bounds errors + max_idx = self.cfg.num_bins - 1 + idx_x = torch.clamp(idx_x, 0, max_idx) + idx_y = torch.clamp(idx_y, 0, max_idx) + + params_r = self.calib_data_grad_r[idx_x, idx_y, :] + params_g = self.calib_data_grad_g[idx_x, idx_y, :] + params_b = self.calib_data_grad_b[idx_x, idx_y, :] + + # Reuse pre-allocated buffer, resize if batch size changed + target_shape = (*idx_x.shape, 3) + if self._sim_img_rgb_buffer.shape != target_shape: + self._sim_img_rgb_buffer = torch.empty(target_shape, device=self.device) + sim_img_rgb = self._sim_img_rgb_buffer + + sim_img_rgb[..., 0] = torch.sum(self.A_tensor * params_r, dim=-1) # R + sim_img_rgb[..., 1] = torch.sum(self.A_tensor * params_g, dim=-1) # G + sim_img_rgb[..., 2] = torch.sum(self.A_tensor * params_b, dim=-1) # B + + # write tactile image + sim_img = sim_img_rgb + self.background_tensor # /255.0 + sim_img = torch.clip(sim_img, 0, 255, out=sim_img).to(torch.uint8) + return sim_img + + """ + Internal Helpers. + """ + + def _get_render_data(self, data_dir: str, file_name: str) -> str: + """Gets the path for the GelSight render data file. + + Args: + data_dir: The data directory name containing the render data. + file_name: The specific file name to retrieve. + + Returns: + The local path to the file. + + Raises: + FileNotFoundError: If the file is not found locally or on Nucleus. + """ + # Construct path using the configured base path + file_path = os.path.join(self.cfg.base_data_path, data_dir, file_name) + + # Cache directory for downloads + cache_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), data_dir) + + # Use retrieve_file_path to handle local/Nucleus paths and caching + return retrieve_file_path(file_path, download_dir=cache_dir, force_download=False) + + def _generate_normals(self, img: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Generate the gradient magnitude and direction of the height map. + + Args: + img: Input height map tensor. Shape: (N, H, W). + + Returns: + Tuple containing gradient magnitude tensor and gradient direction tensor. Shape: (N, H, W). + """ + img_grad = torch.gradient(img, dim=(1, 2)) + dzdx, dzdy = img_grad + + grad_mag_orig = torch.sqrt(dzdx**2 + dzdy**2) + grad_mag = torch.arctan(grad_mag_orig) # seems that arctan is used as a squashing function + grad_dir = torch.arctan2(dzdx, dzdy) + grad_dir[grad_mag_orig == 0] = 0 + + # handle edges + grad_mag = torch.nn.functional.pad(grad_mag[:, 1:-1, 1:-1], pad=(1, 1, 1, 1)) + grad_dir = torch.nn.functional.pad(grad_dir[:, 1:-1, 1:-1], pad=(1, 1, 1, 1)) + + return grad_mag, grad_dir + + def _get_filtering_kernel(self, kernel_size: int = 5) -> np.ndarray: + """Create a Gaussian filtering kernel. + + For kernel derivation, see https://cecas.clemson.edu/~stb/ece847/internal/cvbook/ch03_filtering.pdf + + Args: + kernel_size: Size of the kernel. Defaults to 5. + + Returns: + Filtering kernel. Shape is (kernel_size, kernel_size). + """ + filter_1D = scipy.special.binom(kernel_size - 1, np.arange(kernel_size)) + filter_1D /= filter_1D.sum() + filter_1D = filter_1D[..., None] + + kernel = filter_1D @ filter_1D.T + return kernel + + def _gaussian_filtering(self, img: torch.Tensor, kernel: torch.Tensor) -> torch.Tensor: + """Apply Gaussian filtering to the input image tensor. + + Args: + img: Input image tensor. Shape is (N, H, W, 1). + kernel: Filtering kernel tensor. Shape is (K, K). + + Returns: + Filtered image tensor. Shape is (N, H, W, 1). + """ + img_output = torch.nn.functional.conv2d( + img.permute(0, 3, 1, 2), kernel.unsqueeze(0).unsqueeze(0), stride=1, padding="same" + ).permute(0, 2, 3, 1) + return img_output diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor.py new file mode 100644 index 000000000000..c08d5fe53381 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor.py @@ -0,0 +1,913 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from __future__ import annotations + +import itertools +import logging +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +import numpy as np +import torch + +import isaacsim.core.utils.torch as torch_utils +from isaacsim.core.simulation_manager import SimulationManager +from pxr import Usd, UsdGeom, UsdPhysics + +import isaaclab.sim as sim_utils +import isaaclab.utils.math as math_utils +from isaaclab.markers import VisualizationMarkers +from isaaclab.sensors.camera import Camera, TiledCamera +from isaaclab.sensors.sensor_base import SensorBase + +from .visuotactile_render import GelsightRender +from .visuotactile_sensor_data import VisuoTactileSensorData + +if TYPE_CHECKING: + from .visuotactile_sensor_cfg import VisuoTactileSensorCfg + +import trimesh + +logger = logging.getLogger(__name__) + + +class VisuoTactileSensor(SensorBase): + r"""A tactile sensor for both camera-based and force field tactile sensing. + + This sensor provides: + 1. Camera-based tactile sensing: depth images from tactile surface + 2. Force field tactile sensing: Penalty-based normal and shear forces using SDF queries + + The sensor can be configured to use either or both sensing modalities. + + **Computation Pipeline:** + Camera-based sensing computes depth differences from a nominal (no-contact) baseline and + processes them through the tac-sl GelSight renderer to produce realistic tactile images. + + Force field sensing queries Signed Distance Fields (SDF) to compute penetration depths, + then applies penalty-based spring-damper models + (:math:`F_n = k_n \cdot \text{depth}`, :math:`F_t = \min(k_t \cdot \|v_t\|, \mu \cdot F_n)`) + to compute normal and shear forces at discrete tactile points. + + **Example Usage:** + For a complete working example, see: ``scripts/demos/sensors/tacsl/tacsl_example.py`` + + **Current Limitations:** + - SDF collision meshes must be pre-computed and objects specified before simulation starts + - Force field computation requires specific rigid body and mesh configurations + - No support for dynamic addition/removal of interacting objects during runtime + + Configuration Requirements: + The following requirements must be satisfied for proper sensor operation: + + **Camera Tactile Imaging** + If ``enable_camera_tactile=True``, a valid ``camera_cfg`` (TiledCameraCfg) must be + provided with appropriate camera parameters. + + **Force Field Computation** + If ``enable_force_field=True``, the following parameters are required: + + * ``contact_object_prim_path_expr`` - Prim path expression to find the contact object prim + + **SDF Computation** + When force field computation is enabled, penalty-based normal and shear forces are + computed using Signed Distance Field (SDF) queries. To achieve GPU acceleration: + + * Interacting objects should have pre-computed SDF collision meshes + * An SDFView must be defined during initialization, therefore interacting objects + should be specified before simulation. + + """ + + cfg: VisuoTactileSensorCfg + """The configuration parameters.""" + + def __init__(self, cfg: VisuoTactileSensorCfg): + """Initializes the tactile sensor object. + + Args: + cfg: The configuration parameters. + """ + + # Create empty variables for storing output data + self._data: VisuoTactileSensorData = VisuoTactileSensorData() + + # Camera-based tactile sensing + self._camera_sensor: Camera | TiledCamera | None = None + self._nominal_tactile: dict | None = None + + # Force field tactile sensing + self._tactile_pos_local: torch.Tensor | None = None + self._tactile_quat_local: torch.Tensor | None = None + self._sdf_object: Any | None = None + + # COMs for velocity correction + self._elastomer_com_b: torch.Tensor | None = None + self._contact_object_com_b: torch.Tensor | None = None + + # Physics views + self._physics_sim_view = None + self._elastomer_body_view = None + self._elastomer_tip_view = None + self._contact_object_body_view = None + + # Visualization + self._tactile_visualizer: VisualizationMarkers | None = None + + # Tactile points count + self.num_tactile_points: int = 0 + + # Now call parent class constructor + super().__init__(cfg) + + def __del__(self): + """Unsubscribes from callbacks and detach from the replicator registry.""" + if self._camera_sensor is not None: + self._camera_sensor.__del__() + # unsubscribe from callbacks + super().__del__() + + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"Tactile sensor @ '{self.cfg.prim_path}': \n" + f"\trender config : {self.cfg.render_cfg.base_data_path}/{self.cfg.render_cfg.sensor_data_dir_name}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tcamera enabled : {self.cfg.enable_camera_tactile}\n" + f"\tforce field enabled: {self.cfg.enable_force_field}\n" + f"\tnum instances : {self.num_instances}\n" + ) + + """ + Properties + """ + + @property + def num_instances(self) -> int: + return self._num_envs + + @property + def data(self) -> VisuoTactileSensorData: + # Update sensors if needed + self._update_outdated_buffers() + # Return the data + return self._data + + """ + Operations + """ + + def reset(self, env_ids: Sequence[int] | None = None): + """Resets the sensor internals.""" + # reset the timestamps + super().reset(env_ids) + + # Reset camera sensor if enabled + if self._camera_sensor: + self._camera_sensor.reset(env_ids) + + """ + Implementation + """ + + def _initialize_impl(self): + """Initializes the sensor-related handles and internal buffers.""" + super()._initialize_impl() + + # Obtain global simulation view + self._physics_sim_view = SimulationManager.get_physics_sim_view() + + # Initialize camera-based tactile sensing + if self.cfg.enable_camera_tactile: + self._initialize_camera_tactile() + + # Initialize force field tactile sensing + if self.cfg.enable_force_field: + self._initialize_force_field() + + # Initialize visualization + if self.cfg.debug_vis: + self._initialize_visualization() + + def get_initial_render(self) -> dict | None: + """Get the initial tactile sensor render for baseline comparison. + + This method captures the initial state of the tactile sensor when no contact + is occurring. This baseline is used for computing relative changes during + tactile interactions. + + .. warning:: + It is the user's responsibility to ensure that the sensor is in a "no contact" state + when this method is called. If the sensor is in contact with an object, the baseline + will be incorrect, leading to erroneous tactile readings. + + Returns: + dict | None: Dictionary containing initial render data with sensor output keys + and corresponding tensor values. Returns None if camera tactile + sensing is disabled. + + Raises: + RuntimeError: If camera sensor is not initialized or initial render fails. + """ + if not self.cfg.enable_camera_tactile: + return None + + self._camera_sensor.update(dt=0.0) + + # get the initial render + initial_render = self._camera_sensor.data.output + if initial_render is None: + raise RuntimeError("Initial render is None") + + # Store the initial nominal tactile data + self._nominal_tactile = dict() + for key, value in initial_render.items(): + self._nominal_tactile[key] = value.clone() + + return self._nominal_tactile + + def _initialize_camera_tactile(self): + """Initialize camera-based tactile sensing.""" + if self.cfg.camera_cfg is None: + raise ValueError("Camera configuration is None. Please provide a valid camera configuration.") + # check image size is consistent with the render config + if ( + self.cfg.camera_cfg.height != self.cfg.render_cfg.image_height + or self.cfg.camera_cfg.width != self.cfg.render_cfg.image_width + ): + raise ValueError( + "Camera configuration image size is not consistent with the render config. Camera size:" + f" {self.cfg.camera_cfg.height}x{self.cfg.camera_cfg.width}, Render config:" + f" {self.cfg.render_cfg.image_height}x{self.cfg.render_cfg.image_width}" + ) + # check data types + if not all(data_type in ["distance_to_image_plane", "depth"] for data_type in self.cfg.camera_cfg.data_types): + raise ValueError( + f"Camera configuration data types are not supported. Data types: {self.cfg.camera_cfg.data_types}" + ) + if self.cfg.camera_cfg.update_period != self.cfg.update_period: + logger.warning( + f"Camera configuration update period ({self.cfg.camera_cfg.update_period}) is not equal to sensor" + f" update period ({self.cfg.update_period}), changing camera update period to match sensor update" + " period" + ) + self.cfg.camera_cfg.update_period = self.cfg.update_period + + # gelsightRender + self._tactile_rgb_render = GelsightRender(self.cfg.render_cfg, device=self.device) + + # Create camera sensor + self._camera_sensor = TiledCamera(self.cfg.camera_cfg) + + # Initialize camera + if not self._camera_sensor.is_initialized: + self._camera_sensor._initialize_impl() + self._camera_sensor._is_initialized = True + + # Initialize camera buffers + self._data.tactile_rgb_image = torch.zeros( + (self._num_envs, self.cfg.camera_cfg.height, self.cfg.camera_cfg.width, 3), device=self._device + ) + self._data.tactile_depth_image = torch.zeros( + (self._num_envs, self.cfg.camera_cfg.height, self.cfg.camera_cfg.width, 1), device=self._device + ) + + logger.info("Camera-based tactile sensing initialized.") + + def _initialize_force_field(self): + """Initialize force field tactile sensing components. + + This method sets up all components required for force field based tactile sensing: + + 1. Creates PhysX views for elastomer and contact object rigid bodies + 2. Generates tactile sensing points on the elastomer surface using mesh geometry + 3. Initializes SDF (Signed Distance Field) for collision detection + 4. Creates data buffers for storing force field measurements + + The tactile points are generated by ray-casting onto the elastomer mesh surface + to create a grid of sensing points that will be used for force computation. + + """ + + # Generate tactile points on elastomer surface + self._generate_tactile_points( + num_divs=list(self.cfg.tactile_array_size), + margin=getattr(self.cfg, "tactile_margin", 0.003), + visualize=self.cfg.trimesh_vis_tactile_points, + ) + + self._create_physx_views() + + # Initialize force field data buffers + self._initialize_force_field_buffers() + logger.info("Force field tactile sensing initialized.") + + def _create_physx_views(self) -> None: + """Create PhysX views for contact object and elastomer bodies. + + This method sets up the necessary PhysX views for force field computation: + 1. Creates rigid body view for elastomer + 2. If contact object prim path expression is not None, then: + a. Finds and validates the object prim and its collision mesh + b. Creates SDF view for collision detection + c. Creates rigid body view for object + + """ + elastomer_pattern = self._parent_prims[0].GetPath().pathString.replace("env_0", "env_*") + self._elastomer_body_view = self._physics_sim_view.create_rigid_body_view([elastomer_pattern]) + # Get elastomer COM for velocity correction + self._elastomer_com_b = self._elastomer_body_view.get_coms().to(self._device).split([3, 4], dim=-1)[0] + + if self.cfg.contact_object_prim_path_expr is None: + return + + contact_object_mesh, contact_object_rigid_body = self._find_contact_object_components() + # Create SDF view for collision detection + num_query_points = self.cfg.tactile_array_size[0] * self.cfg.tactile_array_size[1] + mesh_path_pattern = contact_object_mesh.GetPath().pathString.replace("env_0", "env_*") + self._contact_object_sdf_view = self._physics_sim_view.create_sdf_shape_view( + mesh_path_pattern, num_query_points + ) + + # Create rigid body views for contact object and elastomer + body_path_pattern = contact_object_rigid_body.GetPath().pathString.replace("env_0", "env_*") + self._contact_object_body_view = self._physics_sim_view.create_rigid_body_view([body_path_pattern]) + # Get contact object COM for velocity correction + self._contact_object_com_b = self._contact_object_body_view.get_coms().to(self._device).split([3, 4], dim=-1)[0] + + def _find_contact_object_components(self) -> tuple[Any, Any]: + """Find and validate contact object SDF mesh and its parent rigid body. + + This method searches for the contact object prim using the configured filter pattern, + then locates the first SDF collision mesh within that prim hierarchy and + identifies its parent rigid body for physics simulation. + + Returns: + Tuple of (contact_object_mesh, contact_object_rigid_body) + Returns None if contact object components are not found. + + Note: + Only SDF meshes are supported for optimal force field computation performance. + If no SDF mesh is found, the method will log a warning and return None. + """ + # Find the contact object prim using the configured pattern + contact_object_prim = sim_utils.find_first_matching_prim(self.cfg.contact_object_prim_path_expr) + if contact_object_prim is None: + raise RuntimeError( + f"No contact object prim found matching pattern: {self.cfg.contact_object_prim_path_expr}" + ) + + def is_sdf_mesh(prim: Usd.Prim) -> bool: + """Check if a mesh prim is configured for SDF approximation.""" + return ( + prim.HasAPI(UsdPhysics.MeshCollisionAPI) + and UsdPhysics.MeshCollisionAPI(prim).GetApproximationAttr().Get() == "sdf" + ) + + # Find the SDF mesh within the contact object + contact_object_mesh = sim_utils.get_first_matching_child_prim( + contact_object_prim.GetPath(), predicate=is_sdf_mesh + ) + if contact_object_mesh is None: + raise RuntimeError( + f"No SDF mesh found under contact object at path: {contact_object_prim.GetPath().pathString}" + ) + + def find_parent_rigid_body(prim: Usd.Prim) -> Usd.Prim | None: + """Find the first parent prim with RigidBodyAPI.""" + current_prim = prim + while current_prim and current_prim.IsValid(): + if current_prim.HasAPI(UsdPhysics.RigidBodyAPI): + return current_prim + current_prim = current_prim.GetParent() + if current_prim.GetPath() == "/": + break + return None + + # Find the rigid body parent of the SDF mesh + contact_object_rigid_body = find_parent_rigid_body(contact_object_mesh) + if contact_object_rigid_body is None: + raise RuntimeError( + f"No contact object rigid body found for mesh at path: {contact_object_mesh.GetPath().pathString}" + ) + + return contact_object_mesh, contact_object_rigid_body + + def _generate_tactile_points(self, num_divs: list, margin: float, visualize: bool): + """Generate tactile sensing points from elastomer mesh geometry. + + This method creates a grid of tactile sensing points on the elastomer surface + by ray-casting onto the mesh geometry. Visual meshes are used for smoother point sampling. + + Args: + num_divs: Number of divisions [rows, cols] for the tactile grid. + margin: Margin distance from mesh edges in meters. + visualize: Whether to show the generated points in trimesh visualization. + + """ + + # Get the elastomer prim path + elastomer_prim_path = self._parent_prims[0].GetPath().pathString + + def is_visual_mesh(prim) -> bool: + """Check if a mesh prim has visual properties (visual mesh, not collision mesh).""" + return prim.IsA(UsdGeom.Mesh) and not prim.HasAPI(UsdPhysics.CollisionAPI) + + elastomer_mesh_prim = sim_utils.get_first_matching_child_prim(elastomer_prim_path, predicate=is_visual_mesh) + if elastomer_mesh_prim is None: + raise RuntimeError(f"No visual mesh found under elastomer at path: {elastomer_prim_path}") + + logger.info(f"Generating tactile points from USD mesh: {elastomer_mesh_prim.GetPath().pathString}") + + # Extract mesh data + usd_mesh = UsdGeom.Mesh(elastomer_mesh_prim) + points = np.asarray(usd_mesh.GetPointsAttr().Get()) + face_indices = np.asarray(usd_mesh.GetFaceVertexIndicesAttr().Get()) + + # Simple triangulation + faces = face_indices.reshape(-1, 3) + + # Create bounds + mesh_bounds = np.array([points.min(axis=0), points.max(axis=0)]) + + # Create trimesh object + mesh = trimesh.Trimesh(vertices=points, faces=faces) + + # Generate grid on elastomer + elastomer_dims = np.diff(mesh_bounds, axis=0).squeeze() + slim_axis = np.argmin(elastomer_dims) # Determine flat axis of elastomer + + # Determine tip direction using dome geometry + # For dome-shaped elastomers, the center of mass is shifted toward the dome (contact) side + mesh_center_of_mass = mesh.center_mass[slim_axis] + bounding_box_center = (mesh_bounds[0, slim_axis] + mesh_bounds[1, slim_axis]) / 2.0 + + tip_direction_sign = 1.0 if mesh_center_of_mass > bounding_box_center else -1.0 + + # Determine gap between adjacent tactile points + axis_idxs = list(range(3)) + axis_idxs.remove(int(slim_axis)) # Remove slim idx + div_sz = (elastomer_dims[axis_idxs] - margin * 2.0) / (np.array(num_divs) + 1) + tactile_points_dx = min(div_sz) + + # Sample points on the flat plane + planar_grid_points = [] + center = (mesh_bounds[0] + mesh_bounds[1]) / 2.0 + idx = 0 + for axis_i in range(3): + if axis_i == slim_axis: + # On the slim axis, place a point far away so ray is pointing at the elastomer tip + planar_grid_points.append([tip_direction_sign]) + else: + axis_grid_points = np.linspace( + center[axis_i] - tactile_points_dx * (num_divs[idx] + 1.0) / 2.0, + center[axis_i] + tactile_points_dx * (num_divs[idx] + 1.0) / 2.0, + num_divs[idx] + 2, + ) + planar_grid_points.append(axis_grid_points[1:-1]) # Leave out the extreme corners + idx += 1 + + grid_corners = itertools.product(planar_grid_points[0], planar_grid_points[1], planar_grid_points[2]) + grid_corners = np.array(list(grid_corners)) + + # Project ray in positive y direction on the mesh + mesh_data = trimesh.ray.ray_triangle.RayMeshIntersector(mesh) + ray_dir = np.array([0, 0, 0]) + ray_dir[slim_axis] = -tip_direction_sign # Ray points towards elastomer (opposite of tip direction) + + # Handle the ray intersection result + index_tri, index_ray, locations = mesh_data.intersects_id( + grid_corners, np.tile([ray_dir], (grid_corners.shape[0], 1)), return_locations=True, multiple_hits=False + ) + + if visualize: + query_pointcloud = trimesh.PointCloud(locations, colors=(0.0, 0.0, 1.0)) + trimesh.Scene([mesh, query_pointcloud]).show() + + # Sort and store tactile points + tactile_points = locations[index_ray.argsort()] + # in the frame of the elastomer + self._tactile_pos_local = torch.tensor(tactile_points, dtype=torch.float32, device=self._device) + self.num_tactile_points = self._tactile_pos_local.shape[0] + if self.num_tactile_points != self.cfg.tactile_array_size[0] * self.cfg.tactile_array_size[1]: + raise RuntimeError( + f"Number of tactile points does not match expected: {self.num_tactile_points} !=" + f" {self.cfg.tactile_array_size[0] * self.cfg.tactile_array_size[1]}" + ) + + # Assume tactile frame rotation are all the same + rotation = torch.tensor([0, 0, -torch.pi], device=self._device) + self._tactile_quat_local = ( + math_utils.quat_from_euler_xyz(rotation[0], rotation[1], rotation[2]) + .unsqueeze(0) + .repeat(len(tactile_points), 1) + ) + + logger.info(f"Generated {len(tactile_points)} tactile points from USD mesh using ray casting") + + def _initialize_force_field_buffers(self): + """Initialize data buffers for force field sensing.""" + num_pts = self.num_tactile_points + + # Initialize force field data tensors + self._data.tactile_points_pos_w = torch.zeros((self._num_envs, num_pts, 3), device=self._device) + self._data.tactile_points_quat_w = torch.zeros((self._num_envs, num_pts, 4), device=self._device) + self._data.penetration_depth = torch.zeros((self._num_envs, num_pts), device=self._device) + self._data.tactile_normal_force = torch.zeros((self._num_envs, num_pts), device=self._device) + self._data.tactile_shear_force = torch.zeros((self._num_envs, num_pts, 2), device=self._device) + # Pre-compute expanded tactile point tensors to avoid repeated unsqueeze/expand operations + self._tactile_pos_expanded = self._tactile_pos_local.unsqueeze(0).expand(self._num_envs, -1, -1) + self._tactile_quat_expanded = self._tactile_quat_local.unsqueeze(0).expand(self._num_envs, -1, -1) + + def _initialize_visualization(self): + """Initialize visualization markers for tactile points.""" + if self.cfg.visualizer_cfg: + self._visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data. + + This method updates both camera-based and force field tactile sensing data + for the specified environments. + + Args: + env_ids: Sequence of environment indices to update. If length equals + total number of environments, all environments are updated. + """ + # Convert to proper indices for internal methods + if len(env_ids) == self._num_envs: + internal_env_ids = slice(None) + else: + internal_env_ids = env_ids + + # Update camera-based tactile data + if self.cfg.enable_camera_tactile: + self._update_camera_tactile(internal_env_ids) + + # Update force field tactile data + if self.cfg.enable_force_field: + self._update_force_field(internal_env_ids) + + def _update_camera_tactile(self, env_ids: Sequence[int] | slice): + """Update camera-based tactile sensing data. + + This method updates the camera sensor and processes the depth information + to compute tactile measurements. It computes the difference from the nominal + (no-contact) state and renders it using the GelSight tactile renderer. + + Args: + env_ids: Environment indices or slice to update. Can be a sequence of + integers or a slice object for batch processing. + """ + if self._nominal_tactile is None: + raise RuntimeError("Nominal tactile is not set. Please call get_initial_render() first.") + # Update camera sensor + self._camera_sensor.update(self._sim_physics_dt) + + # Get camera data + camera_data = self._camera_sensor.data + + # Check for either distance_to_image_plane or depth (they are equivalent) + depth_key = None + if "distance_to_image_plane" in camera_data.output: + depth_key = "distance_to_image_plane" + elif "depth" in camera_data.output: + depth_key = "depth" + + if depth_key: + self._data.tactile_depth_image[env_ids] = camera_data.output[depth_key][env_ids].clone() + diff = self._nominal_tactile[depth_key][env_ids] - self._data.tactile_depth_image[env_ids] + self._data.tactile_rgb_image[env_ids] = self._tactile_rgb_render.render(diff.squeeze(-1)) + + ######################################################################################### + # Force field tactile sensing + ######################################################################################### + + def _update_force_field(self, env_ids: Sequence[int] | slice): + """Update force field tactile sensing data. + + This method computes penalty-based tactile forces using Signed Distance Field (SDF) + queries. It transforms tactile points to contact object local coordinates, queries the SDF of the + contact object for collision detection, and computes normal and shear forces based on + penetration depth and relative velocities. + + Args: + env_ids: Environment indices or slice to update. Can be a sequence of + integers or a slice object for batch processing. + + Note: + Requires both elastomer and contact object body views to be initialized. Returns + early if tactile points or body views are not available. + """ + # Step 1: Get elastomer pose and precompute pose components + elastomer_pos_w, elastomer_quat_w = self._elastomer_body_view.get_transforms().split([3, 4], dim=-1) + elastomer_quat_w = math_utils.convert_quat(elastomer_quat_w, to="wxyz") + + # Transform tactile points to world coordinates, used for visualization + self._transform_tactile_points_to_world(elastomer_pos_w, elastomer_quat_w) + + # earlly return if contact object body view is not available + # this could happen if the contact object is not specified when tactile_points are required for visualization + if self._contact_object_body_view is None: + return + + # Step 2: Transform tactile points to contact object local frame for SDF queries + contact_object_pos_w, contact_object_quat_w = self._contact_object_body_view.get_transforms().split( + [3, 4], dim=-1 + ) + contact_object_quat_w = math_utils.convert_quat(contact_object_quat_w, to="wxyz") + + world_tactile_points = self._data.tactile_points_pos_w + points_contact_object_local, contact_object_quat_inv = self._transform_points_to_contact_object_local( + world_tactile_points, contact_object_pos_w, contact_object_quat_w + ) + + # Step 3: Query SDF for collision detection + sdf_values_and_gradients = self._contact_object_sdf_view.get_sdf_and_gradients(points_contact_object_local) + sdf_values = sdf_values_and_gradients[..., -1] # Last component is SDF value + sdf_gradients = sdf_values_and_gradients[..., :-1] # First 3 components are gradients + + # Step 4: Compute tactile forces from SDF data + self._compute_tactile_forces_from_sdf( + points_contact_object_local, + sdf_values, + sdf_gradients, + contact_object_pos_w, + contact_object_quat_w, + elastomer_quat_w, + env_ids, + ) + + def _transform_tactile_points_to_world(self, pos_w: torch.Tensor, quat_w: torch.Tensor): + """Transform tactile points from local to world coordinates. + + Args: + pos_w: Elastomer positions in world frame. Shape: (num_envs, 3) + quat_w: Elastomer quaternions in world frame. Shape: (num_envs, 4) + """ + num_pts = self.num_tactile_points + + quat_expanded = quat_w.unsqueeze(1).expand(-1, num_pts, -1) + pos_expanded = pos_w.unsqueeze(1).expand(-1, num_pts, -1) + + # Apply transformation + tactile_pos_w = math_utils.quat_apply(quat_expanded, self._tactile_pos_expanded) + pos_expanded + tactile_quat_w = math_utils.quat_mul(quat_expanded, self._tactile_quat_expanded) + + # Store in data + self._data.tactile_points_pos_w = tactile_pos_w + self._data.tactile_points_quat_w = tactile_quat_w + + def _transform_points_to_contact_object_local( + self, world_points: torch.Tensor, contact_object_pos_w: torch.Tensor, contact_object_quat_w: torch.Tensor + ) -> tuple[torch.Tensor, torch.Tensor]: + """Optimized version: Transform world coordinates to contact object local frame. + + Args: + world_points: Points in world coordinates. Shape: (num_envs, num_points, 3) + contact_object_pos_w: Contact object positions in world frame. Shape: (num_envs, 3) + contact_object_quat_w: Contact object quaternions in world frame. Shape: (num_envs, 4) + + Returns: + Points in contact object local coordinates and inverse quaternions + """ + # Get inverse transformation (per environment) + # wxyz in torch + contact_object_quat_inv, contact_object_pos_inv = torch_utils.tf_inverse( + contact_object_quat_w, contact_object_pos_w + ) + num_pts = self.num_tactile_points + + contact_object_quat_expanded = contact_object_quat_inv.unsqueeze(1).expand(-1, num_pts, 4) + contact_object_pos_expanded = contact_object_pos_inv.unsqueeze(1).expand(-1, num_pts, 3) + + # Apply transformation + points_sdf = torch_utils.tf_apply(contact_object_quat_expanded, contact_object_pos_expanded, world_points) + + return points_sdf, contact_object_quat_inv + + def _get_tactile_points_velocities( + self, linvel_world: torch.Tensor, angvel_world: torch.Tensor, quat_world: torch.Tensor + ) -> torch.Tensor: + """Optimized version: Compute tactile point velocities from precomputed velocities. + + Args: + linvel_world: Elastomer linear velocities. Shape: (num_envs, 3) + angvel_world: Elastomer angular velocities. Shape: (num_envs, 3) + quat_world: Elastomer quaternions. Shape: (num_envs, 4) + + Returns: + Tactile point velocities in world frame. Shape: (num_envs, num_points, 3) + """ + num_pts = self.num_tactile_points + + # Pre-expand all required tensors once + quat_expanded = quat_world.unsqueeze(1).expand(-1, num_pts, 4) + tactile_pos_expanded = self._tactile_pos_expanded + + # Transform local positions to world frame relative vectors + tactile_pos_world_relative = math_utils.quat_apply(quat_expanded, tactile_pos_expanded) + + # Compute velocity due to angular motion: ω × r + angvel_expanded = angvel_world.unsqueeze(1).expand(-1, num_pts, 3) + angular_velocity_contribution = torch.cross(angvel_expanded, tactile_pos_world_relative, dim=-1) + + # Add linear velocity contribution + linvel_expanded = linvel_world.unsqueeze(1).expand(-1, num_pts, 3) + tactile_velocity_world = angular_velocity_contribution + linvel_expanded + + return tactile_velocity_world + + def _compute_tactile_forces_from_sdf( + self, + points_contact_object_local: torch.Tensor, + sdf_values: torch.Tensor, + sdf_gradients: torch.Tensor, + contact_object_pos_w: torch.Tensor, + contact_object_quat_w: torch.Tensor, + elastomer_quat_w: torch.Tensor, + env_ids: Sequence[int] | slice, + ) -> None: + """Optimized version: Compute tactile forces from SDF values using precomputed parameters. + + This method now operates directly on the pre-allocated data tensors to avoid + unnecessary memory allocation and copying. + + Args: + points_contact_object_local: Points in contact object local frame + sdf_values: SDF values (negative means penetration) + sdf_gradients: SDF gradients (surface normals) + contact_object_pos_w: Contact object positions in world frame + contact_object_quat_w: Contact object quaternions in world frame + elastomer_quat_w: Elastomer quaternions + env_ids: Environment indices being updated + + """ + depth = self._data.penetration_depth[env_ids] + tactile_normal_force = self._data.tactile_normal_force[env_ids] + tactile_shear_force = self._data.tactile_shear_force[env_ids] + + # Clear the output tensors + tactile_normal_force.zero_() + tactile_shear_force.zero_() + depth.zero_() + + # Convert SDF values to penetration depth (positive for penetration) + depth[:] = torch.clamp(-sdf_values[env_ids], min=0.0) # Negative SDF means inside (penetrating) + + # Get collision mask for points that are penetrating + collision_mask = depth > 0.0 + + # Use pre-allocated tensors instead of creating new ones + num_pts = self.num_tactile_points + + if collision_mask.any() or self.cfg.visualize_sdf_closest_pts: + # Get contact object and elastomer velocities (com velocities) + contact_object_velocities = self._contact_object_body_view.get_velocities() + contact_object_linvel_w_com = contact_object_velocities[env_ids, :3] + contact_object_angvel_w = contact_object_velocities[env_ids, 3:] + + elastomer_velocities = self._elastomer_body_view.get_velocities() + elastomer_linvel_w_com = elastomer_velocities[env_ids, :3] + elastomer_angvel_w = elastomer_velocities[env_ids, 3:] + + # Contact object adjustment + contact_object_com_w_offset = math_utils.quat_apply( + contact_object_quat_w[env_ids], self._contact_object_com_b[env_ids] + ) + contact_object_linvel_w = contact_object_linvel_w_com - torch.cross( + contact_object_angvel_w, contact_object_com_w_offset, dim=-1 + ) + # v_origin = v_com - w x (com_world_offset) where com_world_offset = quat_apply(quat, com_b) + elastomer_com_w_offset = math_utils.quat_apply(elastomer_quat_w[env_ids], self._elastomer_com_b[env_ids]) + elastomer_linvel_w = elastomer_linvel_w_com - torch.cross( + elastomer_angvel_w, elastomer_com_w_offset, dim=-1 + ) + + # Normalize gradients to get surface normals in local frame + normals_local = torch.nn.functional.normalize(sdf_gradients[env_ids], dim=-1) + + # Transform normals to world frame (rotate by contact object orientation) - use precomputed quaternions + contact_object_quat_expanded = contact_object_quat_w[env_ids].unsqueeze(1).expand(-1, num_pts, 4) + + # Apply quaternion transformation + normals_world = math_utils.quat_apply(contact_object_quat_expanded, normals_local) + + # Compute normal contact force: F_n = k_n * depth + fc_norm = self.cfg.normal_contact_stiffness * depth + fc_world = fc_norm.unsqueeze(-1) * normals_world + + # Get tactile point velocities using precomputed velocities + tactile_velocity_world = self._get_tactile_points_velocities( + elastomer_linvel_w, elastomer_angvel_w, elastomer_quat_w[env_ids] + ) + + # Use precomputed contact object velocities + closest_points_sdf = points_contact_object_local[env_ids] + depth.unsqueeze(-1) * normals_local + + if self.cfg.visualize_sdf_closest_pts: + debug_closest_points_sdf = ( + points_contact_object_local[env_ids] - sdf_values[env_ids].unsqueeze(-1) * normals_local + ) + self.debug_closest_points_wolrd = math_utils.quat_apply( + contact_object_quat_expanded, debug_closest_points_sdf + ) + contact_object_pos_w[env_ids].unsqueeze(1).expand(-1, num_pts, 3) + + contact_object_linvel_expanded = contact_object_linvel_w.unsqueeze(1).expand(-1, num_pts, 3) + contact_object_angvel_expanded = contact_object_angvel_w.unsqueeze(1).expand(-1, num_pts, 3) + closest_points_vel_world = ( + torch.linalg.cross( + contact_object_angvel_expanded, + math_utils.quat_apply(contact_object_quat_expanded, closest_points_sdf), + ) + + contact_object_linvel_expanded + ) + + # Compute relative velocity at contact points + relative_velocity_world = tactile_velocity_world - closest_points_vel_world + + # Compute tangential velocity (perpendicular to normal) + vt_world = relative_velocity_world - normals_world * torch.sum( + normals_world * relative_velocity_world, dim=-1, keepdim=True + ) + vt_norm = torch.norm(vt_world, dim=-1) + + # Compute friction force: F_t = min(k_t * |v_t|, mu * F_n) + ft_static_norm = self.cfg.tangential_stiffness * vt_norm + ft_dynamic_norm = self.cfg.friction_coefficient * fc_norm + ft_norm = torch.minimum(ft_static_norm, ft_dynamic_norm) + + # Apply friction force opposite to tangential velocity + ft_world = -ft_norm.unsqueeze(-1) * vt_world / (vt_norm.unsqueeze(-1).clamp(min=1e-9)) + + # Total tactile force in world frame + tactile_force_world = fc_world + ft_world + + # Transform forces to tactile frame + tactile_force_tactile = math_utils.quat_apply_inverse( + self._data.tactile_points_quat_w[env_ids], tactile_force_world + ) + + # Extract normal and shear components + # Assume tactile frame has Z as normal direction + tactile_normal_force[:] = tactile_force_tactile[..., 2] # Z component + tactile_shear_force[:] = tactile_force_tactile[..., :2] # X,Y components + + ######################################################################################### + # Debug visualization + ######################################################################################### + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization into visualization objects.""" + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + # create markers if necessary for the first time + if self._tactile_visualizer is None: + self._tactile_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self._tactile_visualizer.set_visibility(True) + else: + if self._tactile_visualizer: + self._tactile_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + """Callback for debug visualization of tactile sensor data. + + This method is called during each simulation step when debug visualization is enabled. + It visualizes tactile sensing points as 3D markers in the simulation viewport to help + with debugging and understanding sensor behavior. + + The method handles two visualization modes: + + 1. **Standard mode**: Visualizes ``tactile_points_pos_w`` - the world positions of + tactile sensing points on the sensor surface + 2. **SDF debug mode**: When ``cfg.visualize_sdf_closest_pts`` is True, visualizes + ``debug_closest_points_wolrd`` - the closest surface points computed during + SDF-based force calculations + """ + # Safety check - return if not properly initialized + if not hasattr(self, "_tactile_visualizer") or self._tactile_visualizer is None: + return + vis_points = None + + if self.cfg.visualize_sdf_closest_pts and hasattr(self, "debug_closest_points_wolrd"): + vis_points = self.debug_closest_points_wolrd + else: + vis_points = self._data.tactile_points_pos_w + + if vis_points is None or vis_points.numel() == 0: + return + + viz_points = vis_points.view(-1, 3) # Shape: (num_envs * num_points, 3) + + indices = torch.zeros(viz_points.shape[0], dtype=torch.long, device=self._device) + + marker_scales = torch.ones(viz_points.shape[0], 3, device=self._device) + + # Visualize tactile points + self._tactile_visualizer.visualize(translations=viz_points, marker_indices=indices, scales=marker_scales) diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_cfg.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_cfg.py new file mode 100644 index 000000000000..5aaf9cd67311 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_cfg.py @@ -0,0 +1,192 @@ +# 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 + + +# needed to import for allowing type-hinting: torch.Tensor | None +from __future__ import annotations + +from dataclasses import MISSING + +from isaaclab.markers import VisualizationMarkersCfg +from isaaclab.markers.config import VISUO_TACTILE_SENSOR_MARKER_CFG +from isaaclab.sensors import SensorBaseCfg, TiledCameraCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR + +from .visuotactile_sensor import VisuoTactileSensor + +## +# GelSight Render Configuration +## + + +@configclass +class GelSightRenderCfg: + """Configuration for GelSight sensor rendering parameters. + + This configuration defines the rendering parameters for example-based tactile image synthesis + using the Taxim approach. + + Reference: + Si, Z., & Yuan, W. (2022). Taxim: An example-based simulation model for GelSight + tactile sensors. IEEE Robotics and Automation Letters, 7(2), 2361-2368. + https://arxiv.org/abs/2109.04027 + + Data Directory Structure: + The sensor data should be organized in the following structure:: + + base_data_path/ + └── sensor_data_dir_name/ + ├── bg.jpg # Background image (required) + ├── polycalib.npz # Polynomial calibration data (required) + └── real_bg.npy # Real background data (optional) + + Example: + Using predefined sensor configuration:: + + from isaaclab_contrib.sensors.tacsl_sensor import VisuoTactileSensorCfg + + from isaaclab_assets.sensors import GELSIGHT_R15_CFG + + sensor_cfg = VisuoTactileSensorCfg(render_cfg=GELSIGHT_R15_CFG) + + Using custom sensor data:: + + custom_cfg = GelSightRenderCfg( + base_data_path="/path/to/my/sensors", + sensor_data_dir_name="my_custom_sensor", + image_height=480, + image_width=640, + mm_per_pixel=0.05, + ) + """ + + base_data_path: str = f"{ISAACLAB_NUCLEUS_DIR}/TacSL" + """Base path to the directory containing sensor calibration data. Defaults to + Isaac Lab Nucleus directory at ``{ISAACLAB_NUCLEUS_DIR}/TacSL``. + """ + + sensor_data_dir_name: str = MISSING + """Directory name containing the sensor calibration and background data. + + This should be a relative path (directory name) inside the :attr:`base_data_path`. + """ + + background_path: str = "bg.jpg" + """Filename of the background image within the data directory.""" + + calib_path: str = "polycalib.npz" + """Filename of the polynomial calibration data within the data directory.""" + + real_background: str = "real_bg.npy" + """Filename of the real background data within the data directory.""" + + image_height: int = MISSING + """Height of the tactile image in pixels.""" + + image_width: int = MISSING + """Width of the tactile image in pixels.""" + + num_bins: int = 120 + """Number of bins for gradient magnitude and direction quantization.""" + + mm_per_pixel: float = MISSING + """Millimeters per pixel conversion factor for reconstructing 2D tactile image from the height map.""" + + +## +# Visuo-Tactile Sensor Configuration +## + + +@configclass +class VisuoTactileSensorCfg(SensorBaseCfg): + """Configuration for the visuo-tactile sensor. + + This sensor provides both camera-based tactile sensing and force field tactile sensing. + It can capture tactile RGB/depth images and compute penalty-based contact forces. + """ + + class_type: type = VisuoTactileSensor + + # Sensor type and capabilities + render_cfg: GelSightRenderCfg = MISSING + """Configuration for GelSight sensor rendering. + + This defines the rendering parameters for converting depth maps to realistic tactile images. + + For simplicity, you can use the predefined configs for standard sensor models: + + - :attr:`isaaclab_assets.sensors.GELSIGHT_R15_CFG` + - :attr:`isaaclab_assets.sensors.GELSIGHT_MINI_CFG` + + """ + + enable_camera_tactile: bool = True + """Whether to enable camera-based tactile sensing.""" + + enable_force_field: bool = True + """Whether to enable force field tactile sensing.""" + + # Force field configuration + tactile_array_size: tuple[int, int] = MISSING + """Number of tactile points for force field sensing in (rows, cols) format.""" + + tactile_margin: float = MISSING + """Margin for tactile point generation (in meters). + + This parameter defines the exclusion margin from the edges of the elastomer mesh when generating + the tactile point grid. It ensures that force field points are not generated on the very edges + of the sensor surface where geometry might be unstable or less relevant for contact. + """ + + contact_object_prim_path_expr: str | None = None + """Prim path expression to find the contact object for force field computation. + + This specifies the object that will make contact with the tactile sensor. The sensor will automatically + find the SDF collision mesh within this object for optimal force field computation. + + .. note:: + The expression can contain the environment namespace regex ``{ENV_REGEX_NS}`` which + will be replaced with the environment namespace. + + Example: ``{ENV_REGEX_NS}/ContactObject`` will be replaced with ``/World/envs/env_.*/ContactObject``. + + .. attention:: + For force field computation to work properly, the contact object must have an SDF collision mesh. + The sensor will search for the first SDF mesh within the specified prim hierarchy. + """ + + # Force field physics parameters + normal_contact_stiffness: float = 1.0 + """Normal contact stiffness for penalty-based force computation.""" + + friction_coefficient: float = 2.0 + """Friction coefficient for shear forces.""" + + tangential_stiffness: float = 0.1 + """Tangential stiffness for shear forces.""" + + camera_cfg: TiledCameraCfg | None = None + """Camera configuration for tactile RGB/depth sensing. + + If None, camera-based sensing will be disabled even if :attr:`enable_camera_tactile` is True. + """ + + # Visualization + visualizer_cfg: VisualizationMarkersCfg = VISUO_TACTILE_SENSOR_MARKER_CFG.replace( + prim_path="/Visuals/TactileSensor" + ) + """The configuration object for the visualization markers. + + .. note:: + This attribute is only used when debug visualization is enabled. + """ + + trimesh_vis_tactile_points: bool = False + """Whether to visualize tactile points for debugging using trimesh. Defaults to False.""" + + visualize_sdf_closest_pts: bool = False + """Whether to visualize SDF closest points for debugging. Defaults to False.""" diff --git a/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_data.py b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_data.py new file mode 100644 index 000000000000..1390d0257a0f --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/sensors/tacsl_sensor/visuotactile_sensor_data.py @@ -0,0 +1,49 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from __future__ import annotations + +from dataclasses import dataclass + +import torch + + +@dataclass +class VisuoTactileSensorData: + """Data container for the visuo-tactile sensor. + + This class contains the tactile sensor data that includes: + + - Camera-based tactile sensing (RGB and depth images) + - Force field tactile sensing (normal and shear forces) + - Tactile point positions and contact information + + """ + + # Camera-based tactile data + tactile_depth_image: torch.Tensor | None = None + """Tactile depth images. Shape is (num_instances, height, width, 1).""" + + tactile_rgb_image: torch.Tensor | None = None + """Tactile RGB images rendered using the Taxim approach from :cite:t:`si2022taxim`. + Shape is (num_instances, height, width, 3). + """ + + # Force field tactile data + tactile_points_pos_w: torch.Tensor | None = None + """Positions of tactile points in world frame. Shape is (num_instances, num_tactile_points, 3).""" + + tactile_points_quat_w: torch.Tensor | None = None + """Orientations of tactile points in world frame. Shape is (num_instances, num_tactile_points, 4).""" + + penetration_depth: torch.Tensor | None = None + """Penetration depth at each tactile point. Shape is (num_instances, num_tactile_points).""" + + tactile_normal_force: torch.Tensor | None = None + """Normal forces at each tactile point in sensor frame. Shape is (num_instances, num_tactile_points).""" + + tactile_shear_force: torch.Tensor | None = None + """Shear forces at each tactile point in sensor frame. Shape is (num_instances, num_tactile_points, 2).""" diff --git a/source/isaaclab_contrib/isaaclab_contrib/utils/types.py b/source/isaaclab_contrib/isaaclab_contrib/utils/types.py new file mode 100644 index 000000000000..f6cc242fe6d6 --- /dev/null +++ b/source/isaaclab_contrib/isaaclab_contrib/utils/types.py @@ -0,0 +1,98 @@ +# 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 + +"""Sub-module for multirotor-specific data types. + +This module defines data container classes used for passing multirotor-specific +information between components (e.g., between action terms and actuator models). +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass + +import torch + + +@dataclass +class MultiRotorActions: + """Data container to store multirotor thruster actions. + + This dataclass is used to pass thrust commands and thruster indices between + components in the multirotor control pipeline. It is primarily used internally + by the :class:`~isaaclab_contrib.assets.Multirotor` class to communicate with + :class:`~isaaclab_contrib.actuators.Thruster` actuator models. + + The container supports partial actions by allowing specification of which + thrusters the actions apply to through the :attr:`thruster_indices` field. + + Attributes: + thrusts: Thrust values for the specified thrusters. Shape is typically + ``(num_envs, num_selected_thrusters)``. + thruster_indices: Indices of thrusters that the thrust values apply to. + Can be a tensor of indices, a sequence, a slice, or None for all thrusters. + + Example: + .. code-block:: python + + # Create actions for all thrusters + actions = MultiRotorActions( + thrusts=torch.ones(num_envs, 4) * 5.0, + thruster_indices=slice(None), # All thrusters + ) + + # Create actions for specific thrusters + actions = MultiRotorActions( + thrusts=torch.tensor([[6.0, 7.0]]), + thruster_indices=[0, 2], # Only thrusters 0 and 2 + ) + + Note: + If both fields are ``None``, no action is taken. This is useful for + conditional action application. + + .. seealso:: + - :class:`~isaaclab.utils.types.ArticulationActions`: Similar container for joint actions + - :class:`~isaaclab_contrib.actuators.Thruster`: Thruster actuator that consumes these actions + """ + + thrusts: torch.Tensor | None = None + """Thrust values for the multirotor thrusters. + + Shape: ``(num_envs, num_thrusters)`` or ``(num_envs, num_selected_thrusters)`` + + The units depend on the actuator model configuration: + - For force-based control: Newtons (N) + - For RPS-based control: Revolutions per second (1/s) + + If ``None``, no thrust commands are specified. + """ + + thruster_indices: torch.Tensor | Sequence[int] | slice | None = None + """Indices of thrusters that the thrust values apply to. + + This field specifies which thrusters the :attr:`thrusts` values correspond to. + It can be: + - A torch.Tensor of integer indices: ``torch.tensor([0, 2, 3])`` + - A sequence of integers: ``[0, 2, 3]`` + - A slice: ``slice(None)`` for all thrusters, ``slice(0, 2)`` for first two + - ``None``: Defaults to all thrusters + + Using a slice is more efficient for contiguous thruster ranges as it avoids + creating intermediate index tensors. + + Example: + .. code-block:: python + + # All thrusters (most efficient) + thruster_indices = slice(None) + + # First two thrusters + thruster_indices = slice(0, 2) + + # Specific thrusters + thruster_indices = [0, 2, 3] + """ diff --git a/source/isaaclab_contrib/pyproject.toml b/source/isaaclab_contrib/pyproject.toml new file mode 100644 index 000000000000..d90ac3536f16 --- /dev/null +++ b/source/isaaclab_contrib/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel", "toml"] +build-backend = "setuptools.build_meta" diff --git a/source/isaaclab_contrib/setup.py b/source/isaaclab_contrib/setup.py new file mode 100644 index 000000000000..8de11268f8b9 --- /dev/null +++ b/source/isaaclab_contrib/setup.py @@ -0,0 +1,38 @@ +# 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 + +"""Installation script for the 'isaaclab_contrib' python package.""" + +import os + +import toml +from setuptools import setup + +# Obtain the extension data from the extension.toml file +EXTENSION_PATH = os.path.dirname(os.path.realpath(__file__)) +# Read the extension.toml file +EXTENSION_TOML_DATA = toml.load(os.path.join(EXTENSION_PATH, "config", "extension.toml")) + +# Installation operation +setup( + name="isaaclab_contrib", + author="Isaac Lab Project Developers", + maintainer="Isaac Lab Project Developers", + url=EXTENSION_TOML_DATA["package"]["repository"], + version=EXTENSION_TOML_DATA["package"]["version"], + description=EXTENSION_TOML_DATA["package"]["description"], + keywords=EXTENSION_TOML_DATA["package"]["keywords"], + include_package_data=True, + python_requires=">=3.10", + packages=["isaaclab_contrib"], + classifiers=[ + "Natural Language :: English", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Isaac Sim :: 4.5.0", + "Isaac Sim :: 5.0.0", + ], + zip_safe=False, +) diff --git a/source/isaaclab_contrib/test/actuators/test_thruster.py b/source/isaaclab_contrib/test/actuators/test_thruster.py new file mode 100644 index 000000000000..322005954737 --- /dev/null +++ b/source/isaaclab_contrib/test/actuators/test_thruster.py @@ -0,0 +1,204 @@ +# Copyright (c) 2025-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.app import AppLauncher + +HEADLESS = True + +# if not AppLauncher.instance(): +simulation_app = AppLauncher(headless=HEADLESS).app + +"""Rest of imports follows""" + +from types import SimpleNamespace + +import pytest +import torch + + +def make_thruster_cfg(num_motors: int): + """Create a minimal Thruster-like config object for tests.""" + return SimpleNamespace( + dt=0.01, + num_motors=num_motors, + thrust_range=(0.0, 10.0), + max_thrust_rate=100.0, + thrust_const_range=(0.05, 0.1), + tau_inc_range=(0.01, 0.02), + tau_dec_range=(0.01, 0.02), + torque_to_thrust_ratio=0.0, + use_discrete_approximation=True, + use_rps=True, + integration_scheme="euler", + ) + + +@pytest.mark.parametrize("num_envs", [1, 2, 4]) +@pytest.mark.parametrize("num_motors", [1, 2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_zero_thrust_const_is_handled(num_envs, num_motors, device): + """When thrust_const_range contains zeros, Thruster clamps values and compute returns finite outputs.""" + from isaaclab_contrib.actuators import Thruster + + cfg = make_thruster_cfg(num_motors) + cfg.thrust_const_range = (0.0, 0.0) + + thruster_names = [f"t{i}" for i in range(num_motors)] + thruster_ids = slice(None) + init_rps = torch.ones(num_envs, num_motors, device=device) + + thr = Thruster(cfg, thruster_names, thruster_ids, num_envs, device, init_rps) # type: ignore[arg-type] + + command = torch.full((num_envs, num_motors), 1.0, device=device) + action = SimpleNamespace(thrusts=command.clone(), thruster_indices=thruster_ids) + + thr.compute(action) # type: ignore[arg-type] + + assert torch.isfinite(action.thrusts).all() + + +@pytest.mark.parametrize("num_envs", [1, 2, 4]) +@pytest.mark.parametrize("num_motors", [1, 2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_negative_thrust_range_results_finite(num_envs, num_motors, device): + """Negative configured thrust ranges are clamped and yield finite outputs after hardening.""" + from isaaclab_contrib.actuators import Thruster + + cfg = make_thruster_cfg(num_motors) + cfg.thrust_range = (-5.0, -1.0) + cfg.thrust_const_range = (0.05, 0.05) + + thruster_names = [f"t{i}" for i in range(num_motors)] + thruster_ids = slice(None) + init_rps = torch.ones(num_envs, num_motors, device=device) + + thr = Thruster(cfg, thruster_names, thruster_ids, num_envs, device, init_rps) # type: ignore[arg-type] + + command = torch.full((num_envs, num_motors), -2.0, device=device) + action = SimpleNamespace(thrusts=command.clone(), thruster_indices=thruster_ids) + + thr.compute(action) # type: ignore[arg-type] + + assert torch.isfinite(action.thrusts).all() + + +@pytest.mark.parametrize("num_envs", [2, 3, 4]) +@pytest.mark.parametrize("num_motors", [2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_tensor_vs_slice_indices_and_subset_reset(num_envs, num_motors, device): + """Compute should accept tensor or slice thruster indices, and reset_idx should affect only specified envs.""" + from isaaclab_contrib.actuators import Thruster + + cfg = make_thruster_cfg(num_motors) + + thruster_names = [f"t{i}" for i in range(num_motors)] + # Use motor indices that exist for the given num_motors + motor_indices = [0, min(2, num_motors - 1)] + thruster_ids_tensor = torch.tensor(motor_indices, dtype=torch.int64, device=device) + thruster_ids_slice = slice(None) + init_rps = torch.ones(num_envs, num_motors, device=device) + + thr_tensor = Thruster(cfg, thruster_names, thruster_ids_tensor, num_envs, device, init_rps) # type: ignore[arg-type] + thr_slice = Thruster(cfg, thruster_names, thruster_ids_slice, num_envs, device, init_rps) # type: ignore[arg-type] + + command = torch.full((num_envs, num_motors), cfg.thrust_range[1] * 0.5, device=device) + action_tensor = SimpleNamespace(thrusts=command.clone(), thruster_indices=thruster_ids_tensor) + action_slice = SimpleNamespace(thrusts=command.clone(), thruster_indices=thruster_ids_slice) + + thr_tensor.compute(action_tensor) # type: ignore[arg-type] + thr_slice.compute(action_slice) # type: ignore[arg-type] + + assert action_tensor.thrusts.shape == (num_envs, num_motors) + assert action_slice.thrusts.shape == (num_envs, num_motors) + + # Test reset on the last environment + env_to_reset = num_envs - 1 + prev = thr_tensor.curr_thrust.clone() + thr_tensor.reset_idx(torch.tensor([env_to_reset], dtype=torch.int64, device=device)) + assert not torch.allclose(prev[env_to_reset], thr_tensor.curr_thrust[env_to_reset]) + + +@pytest.mark.parametrize("num_envs", [1, 2, 4]) +@pytest.mark.parametrize("num_motors", [1, 2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_mixing_and_integration_modes(num_envs, num_motors, device): + """Verify mixing factor selection and integration kernel choice reflect the config.""" + from isaaclab_contrib.actuators import Thruster + + cfg = make_thruster_cfg(num_motors) + + thruster_names = [f"t{i}" for i in range(num_motors)] + + # discrete mixing + cfg.use_discrete_approximation = True + cfg.integration_scheme = "euler" + thr_d = Thruster( + cfg, thruster_names, slice(None), num_envs, device, torch.ones(num_envs, num_motors, device=device) + ) # type: ignore[arg-type] + # bound method objects are recreated on access; compare underlying functions instead + assert getattr(thr_d.mixing_factor_function, "__func__", None) is Thruster.discrete_mixing_factor + assert getattr(thr_d._step_thrust, "__func__", None) is Thruster.compute_thrust_with_rpm_time_constant + + # continuous mixing and RK4 + cfg.use_discrete_approximation = False + cfg.integration_scheme = "rk4" + thr_c = Thruster( + cfg, thruster_names, slice(None), num_envs, device, torch.ones(num_envs, num_motors, device=device) + ) # type: ignore[arg-type] + assert getattr(thr_c.mixing_factor_function, "__func__", None) is Thruster.continuous_mixing_factor + assert getattr(thr_c._step_thrust, "__func__", None) is Thruster.compute_thrust_with_rpm_time_constant_rk4 + + +@pytest.mark.parametrize("num_envs", [1, 2, 4]) +@pytest.mark.parametrize("num_motors", [1, 2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_thruster_compute_clamps_and_shapes(num_envs, num_motors, device): + """Thruster.compute should return thrusts with correct shape and within clamp bounds.""" + from isaaclab_contrib.actuators import Thruster + + cfg = make_thruster_cfg(num_motors) + + thruster_names = [f"t{i}" for i in range(num_motors)] + thruster_ids = slice(None) + init_rps = torch.ones(num_envs, num_motors, device=device) + + thr = Thruster(cfg, thruster_names, thruster_ids, num_envs, device, init_rps) # type: ignore[arg-type] + + # command above max to check clamping + command = torch.full((num_envs, num_motors), cfg.thrust_range[1] * 2.0, device=device) + action = SimpleNamespace(thrusts=command.clone(), thruster_indices=thruster_ids) + + out = thr.compute(action) # type: ignore[arg-type] + + assert out.thrusts.shape == (num_envs, num_motors) + # values must be clipped to configured range + assert torch.all(out.thrusts <= cfg.thrust_range[1] + 1e-6) + assert torch.all(out.thrusts >= cfg.thrust_range[0] - 1e-6) + + +@pytest.mark.parametrize("num_envs", [1, 2, 4]) +@pytest.mark.parametrize("num_motors", [1, 2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_thruster_reset_idx_changes_state(num_envs, num_motors, device): + """reset_idx should re-sample parameters for specific env indices.""" + from isaaclab_contrib.actuators import Thruster + + cfg = make_thruster_cfg(num_motors) + + thruster_names = [f"t{i}" for i in range(num_motors)] + thruster_ids = slice(None) + init_rps = torch.ones(num_envs, num_motors, device=device) + + thr = Thruster(cfg, thruster_names, thruster_ids, num_envs, device, init_rps) # type: ignore[arg-type] + + # Mutate an internal sampled parameter so reset produces a measurable change. + thr.tau_inc_s[0, 0] = thr.tau_inc_s[0, 0] + 1.0 + prev_val = thr.tau_inc_s[0, 0].item() + + # reset only environment 0 + thr.reset_idx(torch.tensor([0], dtype=torch.int64, device=device)) + + # at least the first tau_inc value for env 0 should differ from the mutated value + assert not torch.isclose(torch.tensor(prev_val, device=device), thr.tau_inc_s[0, 0]) diff --git a/source/isaaclab_contrib/test/assets/test_multirotor.py b/source/isaaclab_contrib/test/assets/test_multirotor.py new file mode 100644 index 000000000000..84044231b16c --- /dev/null +++ b/source/isaaclab_contrib/test/assets/test_multirotor.py @@ -0,0 +1,415 @@ +# 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 + +# ignore private usage of variables warning +# pyright: reportPrivateUsage=none + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +HEADLESS = True + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import contextlib +import types +import warnings + +import pytest +import torch + +import isaaclab.sim as sim_utils +import isaaclab.sim.utils.prims as prim_utils +from isaaclab.sim import build_simulation_context + +from isaaclab_contrib.assets import Multirotor, MultirotorCfg + +# Best-effort: suppress unraisable destructor warnings emitted during +# teardown of partially-constructed assets in CI/dev environments. We still +# perform explicit cleanup where possible, but filter the remaining noisy +# warnings to keep test output clean. +warnings.filterwarnings("ignore", category=pytest.PytestUnraisableExceptionWarning) + +## +# Pre-defined configs +## +from isaaclab_assets.robots.arl_robot_1 import ARL_ROBOT_1_CFG + + +def generate_multirotor_cfg(usd_path: str | None = None) -> MultirotorCfg: + """Generate a multirotor configuration for tests. + + If an ARL-provided config is available, prefer that. Otherwise return a + minimal `MultirotorCfg` so integration tests can still run when a USD is + provided. + """ + if ARL_ROBOT_1_CFG is not None: + return ARL_ROBOT_1_CFG + + if usd_path is None: + return MultirotorCfg() + + return MultirotorCfg(spawn=sim_utils.UsdFileCfg(usd_path=usd_path)) + + +# ----------------------- +# Unit tests (simulator-free) +# ----------------------- + + +def make_multirotor_stub(num_instances: int, num_thrusters: int, device=torch.device("cpu")): + """Create a lightweight Multirotor instance suitable for unit tests that + don't require IsaacSim. We construct via __new__ and inject minimal + attributes the class methods expect. + """ + # Use a plain object (not a Multirotor instance) to avoid assigning to + # properties that only exist on the real class. We'll bind the + # Multirotor methods we need onto this fake object. + m = types.SimpleNamespace() + # runtime attributes the methods expect + m.device = device + m.num_instances = num_instances + m.num_bodies = 1 + + # allocation matrix as a plain Python list (the Multirotor property will + # convert it to a tensor using `self.cfg.allocation_matrix`), so provide + # it on `m.cfg` like the real object expects. + alloc_list = [[1.0 if r < 2 and c == r else 0.0 for c in range(num_thrusters)] for r in range(6)] + m.cfg = types.SimpleNamespace(allocation_matrix=alloc_list) + # Also provide allocation_matrix directly on the fake object so bound methods + # that access `self.allocation_matrix` succeed (properties won't dispatch + # because `m` is not a real Multirotor instance). + m.allocation_matrix = torch.tensor(alloc_list, device=device) + + # lightweight data container + data = types.SimpleNamespace() + data.default_thruster_rps = torch.zeros(num_instances, num_thrusters, device=device) + data.thrust_target = torch.zeros(num_instances, num_thrusters, device=device) + data.computed_thrust = torch.zeros(num_instances, num_thrusters, device=device) + data.applied_thrust = torch.zeros(num_instances, num_thrusters, device=device) + data.thruster_names = [f"thr_{i}" for i in range(num_thrusters)] + m._data = data + + # combined-wrench buffers + m._thrust_target_sim = torch.zeros_like(m._data.thrust_target) + m._internal_wrench_target_sim = torch.zeros(num_instances, 6, device=device) + m._internal_force_target_sim = torch.zeros(num_instances, m.num_bodies, 3, device=device) + m._internal_torque_target_sim = torch.zeros(num_instances, m.num_bodies, 3, device=device) + + # bind class methods we want to test onto the fake object + m._combine_thrusts = types.MethodType(Multirotor._combine_thrusts, m) + m.set_thrust_target = types.MethodType(Multirotor.set_thrust_target, m) + + return m + + +@pytest.mark.parametrize("num_instances", [1, 2, 4]) +@pytest.mark.parametrize("num_thrusters", [1, 2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_multirotor_combine_thrusts_unit(num_instances, num_thrusters, device): + m = make_multirotor_stub(num_instances=num_instances, num_thrusters=num_thrusters, device=torch.device(device)) + + # Create thrust target with predictable values + thrust_values = torch.arange(1.0, num_instances * num_thrusters + 1.0, device=device).reshape( + num_instances, num_thrusters + ) + m._thrust_target_sim = thrust_values + + # allocation maps first two thrusters to force x and y, rest to zero + # Create allocation matrix: 6 rows (wrench dims) x num_thrusters cols + alloc = [ + [ + 1.0 if r == 0 and c == 0 else 0.0 if c >= 2 else (1.0 if r == 1 and c == 1 else 0.0) + for c in range(num_thrusters) + ] + for r in range(6) + ] + m.cfg.allocation_matrix = alloc + m.allocation_matrix = torch.tensor(alloc, device=device) + + m._combine_thrusts() + + # Expected wrench: thrust @ allocation.T + alloc_t = torch.tensor(alloc, device=device) + expected = torch.matmul(thrust_values, alloc_t.T) + + assert torch.allclose(m._internal_wrench_target_sim, expected) + assert torch.allclose(m._internal_force_target_sim[:, 0, :], expected[:, :3]) + assert torch.allclose(m._internal_torque_target_sim[:, 0, :], expected[:, 3:]) + + +@pytest.mark.parametrize("num_instances", [1, 2, 4]) +@pytest.mark.parametrize("num_thrusters", [1, 2, 4]) +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_set_thrust_target_broadcasting_unit(num_instances, num_thrusters, device): + m = make_multirotor_stub(num_instances=num_instances, num_thrusters=num_thrusters, device=torch.device(device)) + + # Set full-row targets for env 0 + targets = torch.arange(1.0, num_thrusters + 1.0, device=device).unsqueeze(0) + m.set_thrust_target(targets, thruster_ids=slice(None), env_ids=slice(0, 1)) + assert torch.allclose(m._data.thrust_target[0], targets[0]) + + # Set a column across all envs (use integer thruster id so broadcasting works) + # Use the last thruster to avoid index out of bounds + thruster_id = num_thrusters - 1 + column_values = torch.full((num_instances,), 9.0, device=device) + m.set_thrust_target(column_values, thruster_ids=thruster_id, env_ids=slice(None)) + assert torch.allclose(m._data.thrust_target[:, thruster_id], column_values) + + +def test_multirotor_data_annotations(): + from isaaclab_contrib.assets.multirotor.multirotor_data import MultirotorData + + # The class defines attributes for thruster state; the defaults should be None + md = MultirotorData.__new__(MultirotorData) + assert getattr(md, "default_thruster_rps", None) is None + assert getattr(md, "thrust_target", None) is None + assert getattr(md, "applied_thrust", None) is None + + +def test_set_thrust_target_env_slice_unit(): + """Setting targets for an env slice updates only those envs.""" + m = make_multirotor_stub(num_instances=4, num_thrusters=3) + + original = m._data.thrust_target.clone() + targets = torch.tensor([[1.0, 2.0, 3.0]], device=m.device) + # Update envs 1 and 2 + m.set_thrust_target(targets, thruster_ids=slice(None), env_ids=slice(1, 3)) + + assert torch.allclose(m._data.thrust_target[1:3], targets.repeat(2, 1)) + # other envs remain unchanged + assert torch.allclose(m._data.thrust_target[0], original[0]) + assert torch.allclose(m._data.thrust_target[3], original[3]) + + +def test_combine_thrusts_with_zero_allocation(): + """When allocation matrix is zero, combined wrench/force/torque are zero.""" + m = make_multirotor_stub(num_instances=2, num_thrusters=3) + + # zero allocation + zero_alloc = [[0.0 for _ in range(3)] for _ in range(6)] + m.cfg.allocation_matrix = zero_alloc + m.allocation_matrix = torch.zeros(6, 3, device=m.device) + + m._thrust_target_sim = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], device=m.device) + m._combine_thrusts() + + assert torch.allclose(m._internal_wrench_target_sim, torch.zeros_like(m._internal_wrench_target_sim)) + assert torch.allclose(m._internal_force_target_sim, torch.zeros_like(m._internal_force_target_sim)) + assert torch.allclose(m._internal_torque_target_sim, torch.zeros_like(m._internal_torque_target_sim)) + + +def test_arl_cfg_structure_and_counts(): + """Validate the ARL robot config structure (or a safe fallback).""" + # Use the ARL-provided config if available, otherwise synthesize a + # lightweight fallback so this test never skips. + cfg = ARL_ROBOT_1_CFG + if cfg is None: + cfg = types.SimpleNamespace() + # default allocation: 4 thrusters + cfg.allocation_matrix = [[1.0 if r < 2 and c == r else 0.0 for c in range(4)] for r in range(6)] + cfg.actuators = types.SimpleNamespace(thrusters=types.SimpleNamespace(dt=0.01)) + + # allocation matrix must be present and be a list of 6 rows (wrench dims) + assert hasattr(cfg, "allocation_matrix") + alloc = cfg.allocation_matrix + assert alloc is None or isinstance(alloc, list) + if alloc is not None: + assert len(alloc) == 6, "Allocation matrix must have 6 rows (wrench dims)" + assert all(isinstance(r, (list, tuple)) for r in alloc) + num_thr = len(alloc[0]) if len(alloc) > 0 else 0 + assert num_thr > 0, "Allocation matrix must contain at least one thruster column" + + # If actuators metadata exists, it should expose thruster timing values + if hasattr(cfg, "actuators") and cfg.actuators is not None: + thr = getattr(cfg.actuators, "thrusters", None) + if thr is not None: + assert hasattr(thr, "dt") + + +def test_arl_allocation_applies_to_stub(): + """Create a stub with the ARL allocation matrix (or fallback) and verify + `_combine_thrusts` produces the expected internal wrench via matrix + multiplication. + """ + cfg = ARL_ROBOT_1_CFG + if cfg is None or getattr(cfg, "allocation_matrix", None) is None: + # fallback allocation: simple mapping for 4 thrusters + alloc = [[1.0 if r < 2 and c == r else 0.0 for c in range(4)] for r in range(6)] + else: + alloc = cfg.allocation_matrix + + num_thr = len(alloc[0]) + m = make_multirotor_stub(num_instances=2, num_thrusters=num_thr) + # push allocation into the stub (both cfg view and tensor view) + m.cfg.allocation_matrix = alloc + m.allocation_matrix = torch.tensor(alloc, device=m.device) + + # Set a predictable thrust pattern and compute expected wrench manually. + m._thrust_target_sim = torch.tensor([[1.0] * num_thr, [2.0] * num_thr], device=m.device) + m._combine_thrusts() + + # expected: thrusts (N x T) @ allocation.T (T x 6) -> (N x 6) + alloc_t = torch.tensor(alloc, device=m.device) + expected = torch.matmul(m._thrust_target_sim, alloc_t.T) + + assert expected.shape == m._internal_wrench_target_sim.shape + assert torch.allclose(m._internal_wrench_target_sim, expected) + # also check split into force/torque matches + assert torch.allclose(m._internal_force_target_sim[:, 0, :], expected[:, :3]) + assert torch.allclose(m._internal_torque_target_sim[:, 0, :], expected[:, 3:]) + + +def generate_multirotor( + multirotor_cfg: MultirotorCfg, num_multirotors: int, device: str +) -> tuple[Multirotor, torch.Tensor]: + """Create scene prims and spawn `Multirotor` assets from a cfg. + + Mirrors the pattern used in `test_articulation.py`. + """ + translations = torch.zeros(num_multirotors, 3, device=device) + translations[:, 0] = torch.arange(num_multirotors) * 2.5 + + for i in range(num_multirotors): + prim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) + + # Replace the prim_path like other tests do and try to spawn a real + # Multirotor. If creating a full Multirotor fails (missing cfg fields + # or simulator not available) fall back to the simulator-free stub so + # tests can still run and validate behavior without IsaacSim. + try: + multirotor = Multirotor(multirotor_cfg.replace(prim_path="/World/Env_.*/Robot")) + return multirotor, translations + except Exception: + # Determine a reasonable number of thrusters for the stub from the + # cfg allocation matrix if provided, otherwise default to 4. + alloc = getattr(multirotor_cfg, "allocation_matrix", None) + num_thrusters = 4 + if isinstance(alloc, list) and len(alloc) > 0 and isinstance(alloc[0], (list, tuple)): + num_thrusters = len(alloc[0]) + + # Create a simulator-free multirotor stub bound to the same device. + stub = make_multirotor_stub(num_multirotors, num_thrusters, device=torch.device(device)) + return stub, translations + + +@pytest.fixture +def sim(request): + """Create a simulation context for integration tests (app + sim). + + Uses `build_simulation_context` from the project utils so tests match + `test_articulation.py` behaviour. + """ + device = request.getfixturevalue("device") if "device" in request.fixturenames else "cpu" + gravity_enabled = request.getfixturevalue("gravity_enabled") if "gravity_enabled" in request.fixturenames else True + add_ground_plane = ( + request.getfixturevalue("add_ground_plane") if "add_ground_plane" in request.fixturenames else False + ) + + with build_simulation_context( + device=device, auto_add_lighting=True, gravity_enabled=gravity_enabled, add_ground_plane=add_ground_plane + ) as sim: + sim._app_control_on_stop_handle = None + yield sim + + +@pytest.mark.parametrize("num_multirotors", [1]) +@pytest.mark.parametrize("device", ["cpu"]) # restrict to cpu for CI without GPUs +@pytest.mark.isaacsim_ci +def test_multirotor_thruster_buffers_and_actuators(sim, num_multirotors, device): + """Check thruster buffers and actuator wiring in an integration environment. + + This test will be skipped automatically when `ARL_ROBOT_1_CFG` is not + available in the test environment (lightweight setups). + """ + cfg = generate_multirotor_cfg() + + # Try to create either a real multirotor or fall back to a stub; the + # generate_multirotor helper will return a stub when IsaacSim or a full + # cfg is not available so the test never skips. + multirotor, _ = generate_multirotor(cfg, num_multirotors, device=sim.device) + + # If we created a real multirotor, it should be initialized by the test + # scaffolding. If we got a stub, it won't have `is_initialized`. + if hasattr(multirotor, "is_initialized"): + sim.reset() + assert multirotor.is_initialized + + # If thruster buffers exist they should have the expected 2D shape + if hasattr(multirotor, "data") and getattr(multirotor.data, "thrust_target", None) is not None: + assert multirotor.data.thrust_target.ndim == 2 + + # Determine number of thrusters exposed by the asset or stub + try: + num_thr = multirotor.num_thrusters + except Exception: + # Stub exposes `_data` shape + num_thr = multirotor._data.thrust_target.shape[1] + + # Broadcast a simple thrust target and either step the sim (real) or + # emulate the actuator by combining thrusts on the stub. + multirotor.set_thrust_target(torch.ones(num_multirotors, num_thr, device=sim.device)) + if hasattr(multirotor, "update") and hasattr(sim, "step"): + for _ in range(3): + sim.step() + multirotor.update(sim.cfg.dt) + else: + # For the stub, emulate a single actuator update by combining thrusts + if hasattr(multirotor, "_combine_thrusts"): + multirotor._thrust_target_sim = multirotor._data.thrust_target.clone() + multirotor._combine_thrusts() + # set applied_thrust to computed_thrust to mimic an actuator + if hasattr(multirotor._data, "computed_thrust"): + multirotor._data.applied_thrust = multirotor._data.computed_thrust.clone() + + data_container = multirotor.data if hasattr(multirotor, "data") else multirotor._data + assert hasattr(data_container, "applied_thrust") + applied = data_container.applied_thrust + assert applied.shape == (num_multirotors, num_thr) + + +@pytest.mark.parametrize("num_multirotors", [1]) +@pytest.mark.parametrize("device", ["cpu"]) +@pytest.mark.isaacsim_ci +def test_set_thrust_target_broadcasting_integration(sim, num_multirotors, device): + """Ensure `set_thrust_target` broadcasting works in the integration context.""" + cfg = generate_multirotor_cfg() + multirotor, _ = generate_multirotor(cfg, num_multirotors, device=sim.device) + + # Determine number of thrusters for assertion (stub vs real asset) + # try: + # num_thr = multirotor.num_thrusters + # except Exception: + # num_thr = multirotor._data.thrust_target.shape[1] + + # Set a single-thruster column across all envs + multirotor.set_thrust_target( + torch.tensor([9.0] * num_multirotors, device=sim.device), thruster_ids=0, env_ids=slice(None) + ) + # Check that the first column of thrust_target has been updated + data = multirotor.data if hasattr(multirotor, "data") else multirotor._data + assert torch.allclose(data.thrust_target[:, 0], torch.tensor([9.0] * num_multirotors, device=sim.device)) + + # Minimal cleanup to avoid unraisable destructor warnings when a real + # Multirotor was created during the test. + if hasattr(multirotor, "_clear_callbacks"): + try: + for _a in ( + "_prim_deletion_callback_id", + "_initialize_handle", + "_invalidate_initialize_handle", + "_debug_vis_handle", + ): + if not hasattr(multirotor, _a): + setattr(multirotor, _a, None) + multirotor._clear_callbacks() + except Exception: + pass + with contextlib.suppress(Exception): + del multirotor diff --git a/source/isaaclab_contrib/test/sensors/test_visuotactile_render.py b/source/isaaclab_contrib/test/sensors/test_visuotactile_render.py new file mode 100644 index 000000000000..6b9902ea09fa --- /dev/null +++ b/source/isaaclab_contrib/test/sensors/test_visuotactile_render.py @@ -0,0 +1,133 @@ +# 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 + +"""Tests for GelSight utility functions - primarily focused on GelsightRender.""" + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True, enable_cameras=True).app + +import os +import tempfile + +import cv2 +import numpy as np +import pytest +import torch + +from isaaclab_contrib.sensors.tacsl_sensor.visuotactile_render import GelsightRender +from isaaclab_contrib.sensors.tacsl_sensor.visuotactile_sensor_cfg import GelSightRenderCfg + + +def test_gelsight_render_custom_path_missing_file(): + """Test initializing GelsightRender with custom path when file doesn't exist.""" + # Assuming 'non_existent_path' is treated as a local path or Nucleus path + # If we pass a path that definitely doesn't exist locally or on Nucleus, it should fail + cfg = GelSightRenderCfg( + base_data_path="non_existent_path", + sensor_data_dir_name="dummy", + image_height=100, + image_width=100, + mm_per_pixel=0.1, + ) + # This should raise FileNotFoundError because the directory/files won't exist + with pytest.raises(FileNotFoundError): + GelsightRender(cfg, device="cpu") + + +def test_gelsight_render_custom_path_success(): + """Test initializing GelsightRender with valid custom path and files.""" + with tempfile.TemporaryDirectory() as tmpdir: + data_dir = "gelsight_r15_data" + full_dir = os.path.join(tmpdir, data_dir) + os.makedirs(full_dir, exist_ok=True) + + # Create dummy configuration + width, height = 10, 10 + cfg = GelSightRenderCfg( + base_data_path=tmpdir, + sensor_data_dir_name=data_dir, + image_width=width, + image_height=height, + num_bins=5, + mm_per_pixel=0.1, + ) + + # 1. Create dummy background image + bg_path = os.path.join(full_dir, cfg.background_path) + dummy_img = np.zeros((height, width, 3), dtype=np.uint8) + cv2.imwrite(bg_path, dummy_img) + + # 2. Create dummy calibration file + calib_path = os.path.join(full_dir, cfg.calib_path) + # Calibration gradients shape: (num_bins, num_bins, 6) + dummy_grad = np.zeros((cfg.num_bins, cfg.num_bins, 6), dtype=np.float32) + np.savez(calib_path, grad_r=dummy_grad, grad_g=dummy_grad, grad_b=dummy_grad) + + # Test initialization + try: + device = torch.device("cpu") + render = GelsightRender(cfg, device=device) + assert render is not None + assert render.device == device + # Verify loaded background dimensions + assert render.background.shape == (height, width, 3) + except Exception as e: + pytest.fail(f"GelsightRender initialization failed with valid custom files: {e}") + + +@pytest.fixture +def gelsight_render_setup(): + """Fixture to set up GelsightRender for testing with default (Nucleus/Cache) files.""" + # Use default GelSight R1.5 configuration + cfg = GelSightRenderCfg( + sensor_data_dir_name="gelsight_r15_data", image_height=320, image_width=240, mm_per_pixel=0.0877 + ) + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + # Create render instance + try: + render = GelsightRender(cfg, device=device) + yield render, device + except Exception as e: + # If initialization fails (e.g., missing data files), skip tests + pytest.skip(f"GelsightRender initialization failed (likely network/Nucleus issue): {e}") + + +def test_gelsight_render_initialization(gelsight_render_setup): + """Test GelsightRender initialization with default files.""" + render, device = gelsight_render_setup + + # Check that render object was created + assert render is not None + assert render.device == device + + # Check that background was loaded (non-empty) + assert render.background is not None + assert render.background.size > 0 + assert render.background.shape[2] == 3 # RGB + + +def test_gelsight_render_compute(gelsight_render_setup): + """Test the render method of GelsightRender.""" + render, device = gelsight_render_setup + + # Create dummy height map + height, width = render.cfg.image_height, render.cfg.image_width + height_map = torch.zeros((1, height, width), device=device, dtype=torch.float32) + + # Add some features to height map + height_map[0, height // 4 : height // 2, width // 4 : width // 2] = 0.001 # 1mm bump + + # Render + output = render.render(height_map) + + # Check output + assert output is not None + assert output.shape == (1, height, width, 3) + assert output.dtype == torch.uint8 diff --git a/source/isaaclab_contrib/test/sensors/test_visuotactile_sensor.py b/source/isaaclab_contrib/test/sensors/test_visuotactile_sensor.py new file mode 100644 index 000000000000..d9929e7d6e14 --- /dev/null +++ b/source/isaaclab_contrib/test/sensors/test_visuotactile_sensor.py @@ -0,0 +1,450 @@ +# 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 + +# ignore private usage of variables warning +# pyright: reportPrivateUsage=none + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True, enable_cameras=True).app + +"""Rest everything follows.""" + +import math + +import pytest +import torch + +import omni.replicator.core as rep + +import isaaclab.sim as sim_utils +from isaaclab.assets import Articulation, ArticulationCfg, RigidObject, RigidObjectCfg +from isaaclab.sensors.camera import TiledCameraCfg +from isaaclab.terrains.trimesh.utils import make_plane +from isaaclab.terrains.utils import create_prim_from_mesh +from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR + +from isaaclab_contrib.sensors.tacsl_sensor import VisuoTactileSensor, VisuoTactileSensorCfg +from isaaclab_contrib.sensors.tacsl_sensor.visuotactile_sensor_cfg import GelSightRenderCfg + +# Sample sensor poses + +TEST_RENDER_CFG = GelSightRenderCfg( + sensor_data_dir_name="gelsight_r15_data", + image_height=320, + image_width=240, + mm_per_pixel=0.0877, +) + + +def get_sensor_cfg_by_type(sensor_type: str) -> VisuoTactileSensorCfg: + """Return a sensor configuration based on the input type. + + Args: + sensor_type: Type of sensor configuration. Options: "minimum_config", "tactile_cam", "nut_rgb_ff". + + Returns: + The sensor configuration for the specified type. + + Raises: + ValueError: If the sensor_type is not supported. + """ + + if sensor_type == "minimum_config": + return VisuoTactileSensorCfg( + prim_path="/World/Robot/elastomer/sensor_minimum_config", + enable_camera_tactile=False, + enable_force_field=False, + render_cfg=TEST_RENDER_CFG, + tactile_array_size=(10, 10), + tactile_margin=0.003, + ) + elif sensor_type == "tactile_cam": + return VisuoTactileSensorCfg( + prim_path="/World/Robot/elastomer/tactile_cam", + enable_force_field=False, + camera_cfg=TiledCameraCfg( + height=320, + width=240, + prim_path="/World/Robot/elastomer_tip/cam", + update_period=0, + data_types=["distance_to_image_plane"], + spawn=None, + ), + render_cfg=TEST_RENDER_CFG, + tactile_array_size=(10, 10), + tactile_margin=0.003, + ) + + elif sensor_type == "nut_rgb_ff": + return VisuoTactileSensorCfg( + prim_path="/World/Robot/elastomer/sensor_nut", + update_period=0, + debug_vis=False, + enable_camera_tactile=True, + enable_force_field=True, + camera_cfg=TiledCameraCfg( + height=320, + width=240, + prim_path="/World/Robot/elastomer_tip/cam", + update_period=0, + data_types=["distance_to_image_plane"], + spawn=None, + ), + render_cfg=TEST_RENDER_CFG, + tactile_array_size=(5, 10), + tactile_margin=0.003, + contact_object_prim_path_expr="/World/Nut", + ) + + else: + raise ValueError( + f"Unsupported sensor type: {sensor_type}. Supported types: 'minimum_config', 'tactile_cam', 'nut_rgb_ff'" + ) + + +def setup(sensor_type: str = "cube"): + """Create a new stage and setup simulation environment with robot, objects, and sensor. + + Args: + sensor_type: Type of sensor configuration. Options: "minimum_config", "tactile_cam", "nut_rgb_ff". + + Returns: + Tuple containing simulation context, sensor config, timestep, robot config, cube config, and nut config. + """ + # Create a new stage + sim_utils.create_new_stage() + + # Simulation time-step + dt = 0.01 + + # Load kit helper + sim_cfg = sim_utils.SimulationCfg(dt=dt) + sim = sim_utils.SimulationContext(sim_cfg) + + # Ground-plane + mesh = make_plane(size=(100, 100), height=0.0, center_zero=True) + create_prim_from_mesh("/World/defaultGroundPlane", mesh) + + # gelsightr15 filter + usd_file_path = f"{ISAACLAB_NUCLEUS_DIR}/TacSL/gelsight_r15_finger/gelsight_r15_finger.usd" + + # robot + robot_cfg = ArticulationCfg( + prim_path="/World/Robot", + spawn=sim_utils.UsdFileWithCompliantContactCfg( + usd_path=usd_file_path, + rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=True), + compliant_contact_stiffness=10.0, + compliant_contact_damping=1.0, + physics_material_prim_path="elastomer", + ), + actuators={}, + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.5), + rot=(math.sqrt(2) / 2, -math.sqrt(2) / 2, 0.0, 0.0), # 90° rotation + joint_pos={}, + joint_vel={}, + ), + ) + # Cube + cube_cfg = RigidObjectCfg( + prim_path="/World/Cube", + spawn=sim_utils.CuboidCfg( + size=(0.1, 0.1, 0.1), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + ) + # Nut + nut_cfg = RigidObjectCfg( + prim_path="/World/Nut", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Factory/factory_nut_m16.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(disable_gravity=False), + articulation_props=sim_utils.ArticulationRootPropertiesCfg(articulation_enabled=False), + mass_props=sim_utils.MassPropertiesCfg(mass=0.1), + ), + init_state=RigidObjectCfg.InitialStateCfg( + pos=(0.0, 0.0 + 0.06776, 0.52), + rot=(1.0, 0.0, 0.0, 0.0), + ), + ) + + # Get the requested sensor configuration using the factory function + sensor_cfg = get_sensor_cfg_by_type(sensor_type) + + # load stage + sim_utils.update_stage() + return sim, sensor_cfg, dt, robot_cfg, cube_cfg, nut_cfg + + +def teardown(sim): + """Teardown simulation environment.""" + # close all the opened viewport from before. + rep.vp_manager.destroy_hydra_textures("Replicator") + # stop simulation + # note: cannot use self.sim.stop() since it does one render step after stopping!! This doesn't make sense :( + sim._timeline.stop() + # clear the stage + sim.clear_all_callbacks() + sim.clear_instance() + + +@pytest.fixture +def setup_minimum_config(): + """Create simulation context with minimum config sensor.""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup("minimum_config") + yield sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg + teardown(sim) + + +@pytest.fixture +def setup_tactile_cam(): + """Create simulation context with tactile camera sensor.""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup("tactile_cam") + yield sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg + teardown(sim) + + +@pytest.fixture +def setup_nut_rgb_ff(): + """Create simulation context with nut RGB force field sensor.""" + sim, sensor_cfg, dt, robot_cfg, cube_cfg, nut_cfg = setup("nut_rgb_ff") + yield sim, sensor_cfg, dt, robot_cfg, cube_cfg, nut_cfg + teardown(sim) + + +@pytest.mark.isaacsim_ci +def test_sensor_minimum_config(setup_minimum_config): + """Test sensor with minimal configuration (no camera, no force field).""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup_minimum_config + _ = Articulation(cfg=robot_cfg) + sensor_minimum = VisuoTactileSensor(cfg=sensor_cfg) + sim.reset() + # Simulate physics + for _ in range(10): + sim.step() + sensor_minimum.update(dt) + + # check data should be None, since both camera and force field are disabled + assert sensor_minimum.data.tactile_depth_image is None + assert sensor_minimum.data.tactile_rgb_image is None + assert sensor_minimum.data.tactile_points_pos_w is None + assert sensor_minimum.data.tactile_points_quat_w is None + assert sensor_minimum.data.penetration_depth is None + assert sensor_minimum.data.tactile_normal_force is None + assert sensor_minimum.data.tactile_shear_force is None + + # Check reset functionality + sensor_minimum.reset() + + for i in range(10): + sim.step() + sensor_minimum.update(dt) + sensor_minimum.reset(env_ids=[0]) + + +@pytest.mark.isaacsim_ci +def test_sensor_cam_size_false(setup_tactile_cam): + """Test sensor initialization fails with incorrect camera image size.""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup_tactile_cam + sensor_cfg.camera_cfg.height = 80 + _ = VisuoTactileSensor(cfg=sensor_cfg) + with pytest.raises(ValueError) as excinfo: + sim.reset() + assert "Camera configuration image size is not consistent with the render config" in str(excinfo.value) + + +@pytest.mark.isaacsim_ci +def test_sensor_cam_type_false(setup_tactile_cam): + """Test sensor initialization fails with unsupported camera data types.""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup_tactile_cam + sensor_cfg.camera_cfg.data_types = ["rgb"] + _ = VisuoTactileSensor(cfg=sensor_cfg) + with pytest.raises(ValueError) as excinfo: + sim.reset() + assert "Camera configuration data types are not supported" in str(excinfo.value) + + +@pytest.mark.isaacsim_ci +def test_sensor_cam_set(setup_tactile_cam): + """Test sensor with camera configuration using existing camera prim.""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup_tactile_cam + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + sim.reset() + sensor.get_initial_render() + for _ in range(10): + sim.step() + sensor.update(dt, force_recompute=True) + robot.update(dt) + assert sensor.is_initialized + assert sensor.data.tactile_depth_image.shape == (1, 320, 240, 1) + assert sensor.data.tactile_rgb_image.shape == (1, 320, 240, 3) + assert sensor.data.tactile_points_pos_w is None + + sensor.reset() + for _ in range(10): + sim.step() + sensor.update(dt, force_recompute=True) + robot.update(dt) + sensor.reset(env_ids=[0]) + + +@pytest.mark.isaacsim_ci +def test_sensor_cam_set_wrong_prim(setup_tactile_cam): + """Test sensor initialization fails with invalid camera prim path.""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup_tactile_cam + sensor_cfg.camera_cfg.prim_path = "/World/Robot/elastomer_tip/cam_wrong" + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + with pytest.raises(RuntimeError) as excinfo: + sim.reset() + robot.update(dt) + sensor.update(dt) + assert "Could not find prim with path" in str(excinfo.value) + + +@pytest.mark.isaacsim_ci +def test_sensor_cam_new_spawn(setup_tactile_cam): + """Test sensor with camera configuration that spawns a new camera.""" + sim, sensor_cfg, dt, robot_cfg, object_cfg, nut_cfg = setup_tactile_cam + sensor_cfg.camera_cfg.prim_path = "/World/Robot/elastomer_tip/cam_new" + sensor_cfg.camera_cfg.spawn = sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.01, 1.0e5) + ) + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + sim.reset() + sensor.get_initial_render() + for _ in range(10): + sim.step() + sensor.update(dt) + robot.update(dt) + # test lazy sensor update + data = sensor.data + assert data is not None + assert data.tactile_depth_image.shape == (1, 320, 240, 1) + assert data.tactile_rgb_image.shape == (1, 320, 240, 3) + assert data.tactile_points_pos_w is None + + assert sensor.is_initialized + + +@pytest.mark.isaacsim_ci +def test_sensor_rgb_forcefield(setup_nut_rgb_ff): + """Test sensor with both camera and force field enabled, detecting contact forces.""" + sim, sensor_cfg, dt, robot_cfg, cube_cfg, nut_cfg = setup_nut_rgb_ff + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + nut = RigidObject(cfg=nut_cfg) + sim.reset() + sensor.get_initial_render() + for _ in range(10): + sim.step() + sensor.update(dt, force_recompute=True) + robot.update(dt) + nut.update(dt) + # check str + print(sensor) + assert sensor.is_initialized + assert sensor.data.tactile_depth_image.shape == (1, 320, 240, 1) + assert sensor.data.tactile_rgb_image.shape == (1, 320, 240, 3) + assert sensor.data.tactile_points_pos_w.shape == (1, 50, 3) + assert sensor.data.penetration_depth.shape == (1, 50) + assert sensor.data.tactile_normal_force.shape == (1, 50) + assert sensor.data.tactile_shear_force.shape == (1, 50, 2) + sum_depth = torch.sum(sensor.data.penetration_depth) # 0.020887471735477448 + normal_force_sum = torch.sum(sensor.data.tactile_normal_force.abs()) + shear_force_sum = torch.sum(sensor.data.tactile_shear_force.abs()) + assert normal_force_sum > 0.0 + assert sum_depth > 0.0 + assert shear_force_sum > 0.0 + + +@pytest.mark.isaacsim_ci +def test_sensor_no_contact_object(setup_nut_rgb_ff): + """Test sensor with force field but no contact object specified.""" + sim, sensor_cfg, dt, robot_cfg, cube_cfg, nut_cfg = setup_nut_rgb_ff + sensor_cfg.contact_object_prim_path_expr = None + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + nut = RigidObject(cfg=nut_cfg) + sim.reset() + sensor.get_initial_render() + for _ in range(10): + sim.step() + sensor.update(dt, force_recompute=True) + robot.update(dt) + nut.update(dt) + + assert sensor.is_initialized + assert sensor.data.tactile_depth_image.shape == (1, 320, 240, 1) + assert sensor.data.tactile_rgb_image.shape == (1, 320, 240, 3) + assert sensor.data.tactile_points_pos_w.shape == (1, 50, 3) + # check no forces are detected + assert torch.all(torch.abs(sensor.data.penetration_depth) < 1e-9) + assert torch.all(torch.abs(sensor.data.tactile_normal_force) < 1e-9) + assert torch.all(torch.abs(sensor.data.tactile_shear_force) < 1e-9) + + +@pytest.mark.isaacsim_ci +def test_sensor_force_field_contact_object_not_found(setup_nut_rgb_ff): + """Test sensor initialization fails when contact object prim path is not found.""" + sim, sensor_cfg, dt, robot_cfg, cube_cfg, NutCfg = setup_nut_rgb_ff + + sensor_cfg.enable_camera_tactile = False + sensor_cfg.contact_object_prim_path_expr = "/World/Nut/wrong_prim" + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + with pytest.raises(RuntimeError) as excinfo: + sim.reset() + robot.update(dt) + sensor.update(dt) + assert "No contact object prim found matching pattern" in str(excinfo.value) + + +@pytest.mark.isaacsim_ci +def test_sensor_force_field_contact_object_no_sdf(setup_nut_rgb_ff): + """Test sensor initialization fails when contact object has no SDF mesh.""" + sim, sensor_cfg, dt, robot_cfg, cube_cfg, NutCfg = setup_nut_rgb_ff + sensor_cfg.enable_camera_tactile = False + sensor_cfg.contact_object_prim_path_expr = "/World/Cube" + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + cube = RigidObject(cfg=cube_cfg) + with pytest.raises(RuntimeError) as excinfo: + sim.reset() + robot.update(dt) + sensor.update(dt) + cube.update(dt) + assert "No SDF mesh found under contact object at path" in str(excinfo.value) + + +@pytest.mark.isaacsim_ci +def test_sensor_update_period_mismatch(setup_nut_rgb_ff): + """Test sensor with both camera and force field enabled, detecting contact forces.""" + sim, sensor_cfg, dt, robot_cfg, cube_cfg, nut_cfg = setup_nut_rgb_ff + sensor_cfg.update_period = dt + sensor_cfg.camera_cfg.update_period = dt * 2 + robot = Articulation(cfg=robot_cfg) + sensor = VisuoTactileSensor(cfg=sensor_cfg) + nut = RigidObject(cfg=nut_cfg) + sim.reset() + sensor.get_initial_render() + assert sensor.cfg.camera_cfg.update_period == sensor.cfg.update_period + for i in range(10): + sim.step() + sensor.update(dt, force_recompute=True) + robot.update(dt) + nut.update(dt) + assert torch.allclose(sensor._timestamp_last_update, torch.tensor((i + 1) * dt, device=sensor.device)) + assert torch.allclose( + sensor._camera_sensor._timestamp_last_update, torch.tensor((i + 1) * dt, device=sensor.device) + ) diff --git a/source/isaaclab_mimic/config/extension.toml b/source/isaaclab_mimic/config/extension.toml index 1e2b712b6d11..5b498ae58652 100644 --- a/source/isaaclab_mimic/config/extension.toml +++ b/source/isaaclab_mimic/config/extension.toml @@ -1,7 +1,7 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "1.0.15" +version = "1.0.16" # Description category = "isaaclab" diff --git a/source/isaaclab_mimic/docs/CHANGELOG.rst b/source/isaaclab_mimic/docs/CHANGELOG.rst index a27a3d64e381..09b79c8cdd80 100644 --- a/source/isaaclab_mimic/docs/CHANGELOG.rst +++ b/source/isaaclab_mimic/docs/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog --------- + +1.0.16 (2025-11-10) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Add body end effector to Mimic data generation to enable loco-manipulation data generation when a navigation p-controller is provided. + + 1.0.15 (2025-09-25) Fixed diff --git a/source/isaaclab_mimic/isaaclab_mimic/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/__init__.py index 96702c1d43a5..17f1264a6b59 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/__init__.py index 8bd532911013..3ede9a65cb67 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/data_generator.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/data_generator.py index 2dc31e1c1cf8..37fabe4a4b47 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/data_generator.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/data_generator.py @@ -1,17 +1,22 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -""" -Base class for data generator. -""" +"""Base class for data generator.""" + import asyncio +import copy +import logging +from typing import Any + import numpy as np import torch -from typing import Any import isaaclab.utils.math as PoseUtils + +logger = logging.getLogger(__name__) + from isaaclab.envs import ( ManagerBasedRLMimicEnv, MimicEnvCfg, @@ -129,15 +134,15 @@ def get_delta_pose_with_scheme( class DataGenerator: - """ - The main data generator class that generates new trajectories from source datasets. + """The main data generator class that generates new trajectories from source datasets. - The data generator, inspired by the MimicGen, enables the generation of new datasets based on a few human - collected source demonstrations. + The data generator, inspired by the MimicGen, enables the generation of new datasets based on a + few human collected source demonstrations. - The data generator works by parsing demonstrations into object-centric subtask segments, stored in DataGenInfoPool. - It then adapts these subtask segments to new scenes by transforming each segment according to the new scene’s context, - stitching them into a coherent trajectory for a robotic end-effector to execute. + The data generator works by parsing demonstrations into object-centric subtask segments, stored in + :class:`DataGenInfoPool`. It then adapts these subtask segments to new scenes by transforming each + segment according to the new scene's context, stitching them into a coherent trajectory for a robotic + end-effector to execute. """ def __init__( @@ -152,8 +157,8 @@ def __init__( env: environment to use for data generation src_demo_datagen_info_pool: source demo datagen info pool dataset_path: path to hdf5 dataset to use for generation - demo_keys: list of demonstration keys to use in file. If not provided, all demonstration keys - will be used. + demo_keys: list of demonstration keys to use in file. If not provided, + all demonstration keys will be used. """ self.env = env self.env_cfg = env.cfg @@ -178,19 +183,14 @@ def __init__( raise ValueError("Either src_demo_datagen_info_pool or dataset_path must be provided") def __repr__(self): - """ - Pretty print this object. - """ + """Pretty print this object.""" msg = str(self.__class__.__name__) - msg += " (\n\tdataset_path={}\n\tdemo_keys={}\n)".format( - self.dataset_path, - self.demo_keys, - ) + msg += f" (\n\tdataset_path={self.dataset_path}\n\tdemo_keys={self.demo_keys}\n)" return msg def randomize_subtask_boundaries(self) -> dict[str, np.ndarray]: - """ - Apply random offsets to sample subtask boundaries according to the task spec. + """Apply random offsets to sample subtask boundaries according to the task spec. + Recall that each demonstration is segmented into a set of subtask segments, and the end index (and start index when skillgen is enabled) of each subtask can have a random offset. """ @@ -236,9 +236,9 @@ def randomize_subtask_boundaries(self) -> dict[str, np.ndarray]: assert np.all((subtask_boundaries[:, :, 1] - subtask_boundaries[:, :, 0]) > 0), "got empty subtasks!" # Ensure subtask indices increase (both starts and ends) - assert np.all( - (subtask_boundaries[:, 1:, :] - subtask_boundaries[:, :-1, :]) > 0 - ), "subtask indices do not strictly increase" + assert np.all((subtask_boundaries[:, 1:, :] - subtask_boundaries[:, :-1, :]) > 0), ( + "subtask indices do not strictly increase" + ) # Ensure subtasks are in order subtask_inds_flat = subtask_boundaries.reshape(subtask_boundaries.shape[0], -1) @@ -258,20 +258,20 @@ def select_source_demo( selection_strategy_name: str, selection_strategy_kwargs: dict | None = None, ) -> int: - """ - Helper method to run source subtask segment selection. + """Helper method to run source subtask segment selection. Args: eef_name: name of end effector eef_pose: current end effector pose object_pose: current object pose for this subtask - src_demo_current_subtask_boundaries: start and end indices for subtask segment in source demonstrations of shape (N, 2) + src_demo_current_subtask_boundaries: start and end indices for subtask segment + in source demonstrations of shape (N, 2) subtask_object_name: name of reference object for this subtask selection_strategy_name: name of selection strategy selection_strategy_kwargs: extra kwargs for running selection strategy Returns: - selected_src_demo_ind: selected source demo index + The selected source demo index """ if subtask_object_name is None: # no reference object - only random selection is supported @@ -333,8 +333,7 @@ def generate_eef_subtask_trajectory( runtime_subtask_constraints_dict: dict, selected_src_demo_inds: dict, ) -> WaypointTrajectory: - """ - Build a transformed waypoint trajectory for a single subtask of an end-effector. + """Build a transformed waypoint trajectory for a single subtask of an end-effector. This method selects a source demonstration segment for the specified subtask, slices the corresponding EEF poses/targets/gripper actions using the randomized @@ -401,22 +400,23 @@ def generate_eef_subtask_trajectory( # The concurrent task has started, so we should use the same source demo selected_src_demo_inds[eef_name] = concurrent_selected_src_ind need_source_demo_selection = False - # This transform is set at after the first data generation iteration/first run of the main while loop + # This transform is set at after the first data generation iteration/first + # run of the main while loop use_delta_transform = runtime_subtask_constraints_dict[ (concurrent_task_spec_key, concurrent_subtask_ind) ]["transform"] else: - assert ( - "transform" not in runtime_subtask_constraints_dict[(eef_name, subtask_ind)] - ), "transform should not be set for concurrent task" + assert "transform" not in runtime_subtask_constraints_dict[(eef_name, subtask_ind)], ( + "transform should not be set for concurrent task" + ) # Need to transform demo according to scheme coord_transform_scheme = runtime_subtask_constraints_dict[(eef_name, subtask_ind)][ "coordination_scheme" ] if coord_transform_scheme != SubTaskConstraintCoordinationScheme.REPLAY: - assert ( - subtask_object_name is not None - ), f"object reference should not be None for {coord_transform_scheme} coordination scheme" + assert subtask_object_name is not None, ( + f"object reference should not be None for {coord_transform_scheme} coordination scheme" + ) if need_source_demo_selection: selected_src_demo_inds[eef_name] = self.select_source_demo( @@ -442,9 +442,9 @@ def generate_eef_subtask_trajectory( if (eef_name, subtask_ind) in runtime_subtask_constraints_dict: if runtime_subtask_constraints_dict[(eef_name, subtask_ind)]["type"] == SubTaskConstraintType.COORDINATION: # Store selected source demo ind for concurrent task - runtime_subtask_constraints_dict[(eef_name, subtask_ind)][ - "selected_src_demo_ind" - ] = selected_src_demo_ind + runtime_subtask_constraints_dict[(eef_name, subtask_ind)]["selected_src_demo_ind"] = ( + selected_src_demo_ind + ) concurrent_task_spec_key = runtime_subtask_constraints_dict[(eef_name, subtask_ind)][ "concurrent_task_spec_key" ] @@ -543,8 +543,7 @@ def merge_eef_subtask_trajectory( prev_executed_traj: list[Waypoint] | None, subtask_trajectory: WaypointTrajectory, ) -> list[Waypoint]: - """ - Merge a subtask trajectory into an executable trajectory for the robot end-effector. + """Merge a subtask trajectory into an executable trajectory for the robot end-effector. This constructs a new `WaypointTrajectory` by first creating an initial interpolation segment, then merging the provided `subtask_trajectory` onto it. @@ -573,7 +572,7 @@ def merge_eef_subtask_trajectory( Trajectory segment for the current subtask that will be merged after the initial interpolation segment. Returns: - list[Waypoint]: The full sequence of waypoints to execute (initial interpolation segment followed by the subtask segment), + The full sequence of waypoints to execute (initial interpolation segment followed by the subtask segment), with the temporary initial waypoint removed. """ is_first_subtask = subtask_index == 0 @@ -625,8 +624,7 @@ async def generate( # noqa: C901 export_demo: bool = True, motion_planner: Any | None = None, ) -> dict: - """ - Attempt to generate a new demonstration. + """Attempt to generate a new demonstration. Args: env_id: environment ID @@ -638,15 +636,16 @@ async def generate( # noqa: C901 motion_planner: motion planner to use for motion planning Returns: - results (dict): dictionary with the following items: - initial_state (dict): initial simulator state for the executed trajectory - states (list): simulator state at each timestep - observations (list): observation dictionary at each timestep - datagen_infos (list): datagen_info at each timestep - actions (np.array): action executed at each timestep - success (bool): whether the trajectory successfully solved the task or not - src_demo_inds (list): list of selected source demonstration indices for each subtask - src_demo_labels (np.array): same as @src_demo_inds, but repeated to have a label for each timestep of the trajectory + A dictionary containing the following items: + - initial_state (dict): initial simulator state for the executed trajectory + - states (list): simulator state at each timestep + - observations (list): observation dictionary at each timestep + - datagen_infos (list): datagen_info at each timestep + - actions (np.array): action executed at each timestep + - success (bool): whether the trajectory successfully solved the task or not + - src_demo_inds (list): list of selected source demonstration indices for each subtask + - src_demo_labels (np.array): same as @src_demo_inds, but repeated to have a label for + each timestep of the trajectory. """ # With skillgen, a motion planner is required to generate collision-free transitions between subtasks. if self.env_cfg.datagen_config.use_skillgen and motion_planner is None: @@ -688,6 +687,10 @@ async def generate( # noqa: C901 eef_subtasks_done[eef_name] = False prev_src_demo_datagen_info_pool_size = 0 + + if self.env_cfg.datagen_config.use_navigation_controller: + was_navigating = False + # While loop that runs per time step while True: async with self.src_demo_datagen_info_pool.asyncio_lock: @@ -732,7 +735,8 @@ async def generate( # noqa: C901 eef_name, current_eef_subtask_indices[eef_name], self.env.cfg ) - # Plan motion using motion planner with comprehensive world update and attachment handling + # Plan motion using motion planner with comprehensive world update + # and attachment handling if motion_planner: print(f"\n--- Environment {env_id}: Planning motion to target pose ---") print(f"Target pose: {target_eef_pose}") @@ -786,7 +790,8 @@ async def generate( # noqa: C901 ) current_eef_subtask_step_indices[eef_name] = 0 else: - # Motion-planned trajectory has been executed, so we are ready to move to execute the next subtask + # Motion-planned trajectory has been executed, so we are ready to move to + # execute the next subtask print("Finished executing motion-planned trajectory") # It is important to pass the prev_executed_traj to merge_eef_subtask_trajectory # so that it can correctly interpolate from the last pose of the motion-planned trajectory @@ -880,8 +885,54 @@ async def generate( # noqa: C901 generated_actions.extend(exec_results["actions"]) generated_success = generated_success or exec_results["success"] + # Get the navigation state + if self.env_cfg.datagen_config.use_navigation_controller: + processed_nav_subtask = False + navigation_state = self.env.get_navigation_state(env_id) + assert navigation_state is not None, "Navigation state cannot be None when using navigation controller" + is_navigating = navigation_state["is_navigating"] + navigation_goal_reached = navigation_state["navigation_goal_reached"] + for eef_name in self.env_cfg.subtask_configs.keys(): current_eef_subtask_step_indices[eef_name] += 1 + + # Execute locomanip navigation controller if it is enabled via the use_navigation_controller flag + if self.env_cfg.datagen_config.use_navigation_controller: + if "body" not in self.env_cfg.subtask_configs.keys(): + error_msg = ( + 'End effector with name "body" not found in subtask configs. "body" must be a valid end' + " effector to use the navigation controller.\n" + ) + logger.error(error_msg) + raise RuntimeError(error_msg) + + # Repeat the last nav subtask action if the robot is navigating and hasn't reached the waypoint goal + if ( + current_eef_subtask_step_indices["body"] == len(current_eef_subtask_trajectories["body"]) - 1 + and not processed_nav_subtask + ): + if is_navigating and not navigation_goal_reached: + for name in self.env_cfg.subtask_configs.keys(): + current_eef_subtask_step_indices[name] -= 1 + processed_nav_subtask = True + + # Else skip to the end of the nav subtask if the robot has reached the waypoint goal before the end + # of the human recorded trajectory + elif was_navigating and not is_navigating and not processed_nav_subtask: + number_of_steps_to_skip = len(current_eef_subtask_trajectories["body"]) - ( + current_eef_subtask_step_indices["body"] + 1 + ) + for name in self.env_cfg.subtask_configs.keys(): + if current_eef_subtask_step_indices[name] + number_of_steps_to_skip < len( + current_eef_subtask_trajectories[name] + ): + current_eef_subtask_step_indices[name] = ( + current_eef_subtask_step_indices[name] + number_of_steps_to_skip + ) + else: + current_eef_subtask_step_indices[name] = len(current_eef_subtask_trajectories[name]) - 1 + processed_nav_subtask = True + subtask_ind = current_eef_subtask_indices[eef_name] if current_eef_subtask_step_indices[eef_name] == len( current_eef_subtask_trajectories[eef_name] @@ -923,6 +974,10 @@ async def generate( # noqa: C901 else: current_eef_subtask_step_indices[eef_name] = None current_eef_subtask_indices[eef_name] += 1 + + if self.env_cfg.datagen_config.use_navigation_controller: + was_navigating = copy.deepcopy(is_navigating) + # Check if all eef_subtasks_done values are True if all(eef_subtasks_done.values()): break diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info.py index 66faa8cc138e..7e94f3e93838 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info.py @@ -1,26 +1,28 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -""" -Defines structure of information that is needed from an environment for data generation. -""" +"""Defines the structure of information required from an environment for data generation processes.""" + from copy import deepcopy class DatagenInfo: - """ - Defines the structure of information required from an environment for data generation processes. - The `DatagenInfo` class centralizes all essential data elements needed for data generation in one place, - reducing the overhead and complexity of repeatedly querying the environment whenever this information is needed. + """Defines the structure of information required from an environment for data generation processes. + + The :class:`DatagenInfo` class centralizes all essential data elements needed for data generation in one place, + reducing the overhead and complexity of repeatedly querying the environment whenever this information + is needed. To allow for flexibility,not all information must be present. Core Elements: + - **eef_pose**: Captures the current 6 dimensional poses of the robot's end-effector. - **object_poses**: Captures the 6 dimensional poses of relevant objects in the scene. - - **subtask_start_signals**: Captures subtask start signals. Used by skillgen to identify the precise start of a subtask from a demonstration. + - **subtask_start_signals**: Captures subtask start signals. Used by skillgen to identify + the precise start of a subtask from a demonstration. - **subtask_term_signals**: Captures subtask completions signals. - **target_eef_pose**: Captures the target 6 dimensional poses for robot's end effector at each time step. - **gripper_action**: Captures the gripper's state. @@ -35,7 +37,8 @@ def __init__( target_eef_pose=None, gripper_action=None, ): - """ + """Initialize the DatagenInfo object. + Args: eef_pose (torch.Tensor or None): robot end effector poses of shape [..., 4, 4] object_poses (dict or None): dictionary mapping object name to object poses @@ -87,9 +90,11 @@ def __init__( if gripper_action is not None: self.gripper_action = gripper_action - def to_dict(self): - """ - Convert this instance to a dictionary containing the same information. + def to_dict(self) -> dict: + """Convert this instance to a dictionary containing the same information. + + Returns: + A dictionary containing the same information as this instance. """ ret = dict() if self.eef_pose is not None: diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info_pool.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info_pool.py index 3cb8d740a86f..5901944929d6 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info_pool.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/datagen_info_pool.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/generation.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/generation.py index 6abdc088170d..18d1f6716d4a 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/generation.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/generation.py @@ -1,16 +1,20 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 import asyncio import contextlib -import torch +import sys +import traceback from typing import Any +import torch + from isaaclab.envs import ManagerBasedRLMimicEnv from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg from isaaclab.managers import DatasetExportMode, TerminationTermCfg +from isaaclab.managers.recorder_manager import RecorderManagerBaseCfg from isaaclab_mimic.datagen.data_generator import DataGenerator from isaaclab_mimic.datagen.datagen_info_pool import DataGenInfoPool @@ -47,14 +51,20 @@ async def run_data_generator( """ global num_success, num_failures, num_attempts while True: - results = await data_generator.generate( - env_id=env_id, - success_term=success_term, - env_reset_queue=env_reset_queue, - env_action_queue=env_action_queue, - pause_subtask=pause_subtask, - motion_planner=motion_planner, - ) + try: + results = await data_generator.generate( + env_id=env_id, + success_term=success_term, + env_reset_queue=env_reset_queue, + env_action_queue=env_action_queue, + pause_subtask=pause_subtask, + motion_planner=motion_planner, + ) + except Exception as e: + sys.stderr.write(traceback.format_exc()) + sys.stderr.flush() + raise e + if bool(results["success"]): num_success += 1 else: @@ -84,7 +94,6 @@ def env_loop( # simulate environment -- run everything in inference mode with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): while True: - # check if any environment needs to be reset while waiting for actions while env_action_queue.qsize() != env.num_envs: asyncio_event_loop.run_until_complete(asyncio.sleep(0)) @@ -141,6 +150,7 @@ def setup_env_config( num_envs: int, device: str, generation_num_trials: int | None = None, + recorder_cfg: RecorderManagerBaseCfg | None = None, ) -> tuple[Any, Any]: """Configure the environment for data generation. @@ -180,7 +190,10 @@ def setup_env_config( env_cfg.observations.policy.concatenate_terms = False # Setup recorders - env_cfg.recorders = ActionStateRecorderManagerCfg() + if recorder_cfg is None: + env_cfg.recorders = ActionStateRecorderManagerCfg() + else: + env_cfg.recorders = recorder_cfg env_cfg.recorders.dataset_export_dir_path = output_dir env_cfg.recorders.dataset_filename = output_file_name diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/selection_strategy.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/selection_strategy.py index 5057bfaa2b96..7c8683c64379 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/selection_strategy.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/selection_strategy.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -7,7 +7,9 @@ Selection strategies used by Isaac Lab Mimic to select subtask segments from source human demonstrations. """ + import abc # for abstract base class definitions + import torch import isaaclab.utils.math as PoseUtils diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/utils.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/utils.py index 7c66ca10ffd8..5430b75597cb 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/utils.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -64,18 +64,20 @@ def get_parameter_input( full_param_name = f"{event_term_name}.{param_name}" if event_term_name else param_name # Create container with label and range slider - container = widgets.HBox([ - widgets.Label(full_param_name, layout=widgets.Layout(width="auto")), - widgets.FloatRangeSlider( - value=[current_val[0], current_val[1]], - min=allowed_range[0], - max=allowed_range[1], - step=step_size, - layout=widgets.Layout(width="300px"), - readout=True, - readout_format=".3f", - ), - ]) + container = widgets.HBox( + [ + widgets.Label(full_param_name, layout=widgets.Layout(width="auto")), + widgets.FloatRangeSlider( + value=[current_val[0], current_val[1]], + min=allowed_range[0], + max=allowed_range[1], + step=step_size, + layout=widgets.Layout(width="300px"), + readout=True, + readout_format=".3f", + ), + ] + ) def on_value_change(change): new_tuple = (change["new"][0], change["new"][1]) @@ -97,18 +99,20 @@ def on_value_change(change): full_param_name = f"{event_term_name}.{param_name}" if event_term_name else param_name # Create container with label and slider - container = widgets.HBox([ - widgets.Label(full_param_name, layout=widgets.Layout(width="auto")), - widgets.FloatSlider( - value=current_val, - min=allowed_range[0], - max=allowed_range[1], - step=step_size, - layout=widgets.Layout(width="300px"), - readout=True, - readout_format=".3f", - ), - ]) + container = widgets.HBox( + [ + widgets.Label(full_param_name, layout=widgets.Layout(width="auto")), + widgets.FloatSlider( + value=current_val, + min=allowed_range[0], + max=allowed_range[1], + step=step_size, + layout=widgets.Layout(width="300px"), + readout=True, + readout_format=".3f", + ), + ] + ) def on_value_change(change): update_fn(change["new"]) diff --git a/source/isaaclab_mimic/isaaclab_mimic/datagen/waypoint.py b/source/isaaclab_mimic/isaaclab_mimic/datagen/waypoint.py index 4cb421eeb8f9..964cc2a49dda 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/datagen/waypoint.py +++ b/source/isaaclab_mimic/isaaclab_mimic/datagen/waypoint.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -6,11 +6,13 @@ """ A collection of classes used to represent waypoints and trajectories. """ + import asyncio import inspect -import torch from copy import deepcopy +import torch + import isaaclab.utils.math as PoseUtils from isaaclab.envs import ManagerBasedRLMimicEnv from isaaclab.managers import TerminationTermCfg @@ -323,7 +325,8 @@ def merge( if need_fixed: # segment of constant target poses equal to @other's first target pose - # account for the fact that we pop'd the first element of @other in anticipation of an interpolation segment + # account for the fact that we pop'd the first element of + # @other in anticipation of an interpolation segment num_steps_fixed_to_use = num_steps_fixed if need_interp else (num_steps_fixed + 1) self.add_waypoint_sequence_for_target_pose( pose=target_for_interpolation.pose, diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py index bc573b58d516..f3a2705a68b6 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -7,63 +7,55 @@ import gymnasium as gym -from .franka_bin_stack_ik_rel_mimic_env_cfg import FrankaBinStackIKRelMimicEnvCfg -from .franka_stack_ik_abs_mimic_env import FrankaCubeStackIKAbsMimicEnv -from .franka_stack_ik_abs_mimic_env_cfg import FrankaCubeStackIKAbsMimicEnvCfg -from .franka_stack_ik_rel_blueprint_mimic_env_cfg import FrankaCubeStackIKRelBlueprintMimicEnvCfg -from .franka_stack_ik_rel_mimic_env import FrankaCubeStackIKRelMimicEnv -from .franka_stack_ik_rel_mimic_env_cfg import FrankaCubeStackIKRelMimicEnvCfg -from .franka_stack_ik_rel_skillgen_env_cfg import FrankaCubeStackIKRelSkillgenEnvCfg -from .franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg import FrankaCubeStackIKRelVisuomotorCosmosMimicEnvCfg -from .franka_stack_ik_rel_visuomotor_mimic_env_cfg import FrankaCubeStackIKRelVisuomotorMimicEnvCfg - ## # Inverse Kinematics - Relative Pose Control ## gym.register( id="Isaac-Stack-Cube-Franka-IK-Rel-Mimic-v0", - entry_point="isaaclab_mimic.envs:FrankaCubeStackIKRelMimicEnv", + entry_point=f"{__name__}.franka_stack_ik_rel_mimic_env:FrankaCubeStackIKRelMimicEnv", kwargs={ - "env_cfg_entry_point": franka_stack_ik_rel_mimic_env_cfg.FrankaCubeStackIKRelMimicEnvCfg, + "env_cfg_entry_point": f"{__name__}.franka_stack_ik_rel_mimic_env_cfg:FrankaCubeStackIKRelMimicEnvCfg", }, disable_env_checker=True, ) gym.register( id="Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-Mimic-v0", - entry_point="isaaclab_mimic.envs:FrankaCubeStackIKRelMimicEnv", + entry_point=f"{__name__}.franka_stack_ik_rel_mimic_env:FrankaCubeStackIKRelMimicEnv", kwargs={ - "env_cfg_entry_point": franka_stack_ik_rel_blueprint_mimic_env_cfg.FrankaCubeStackIKRelBlueprintMimicEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.franka_stack_ik_rel_blueprint_mimic_env_cfg:FrankaCubeStackIKRelBlueprintMimicEnvCfg" + ), }, disable_env_checker=True, ) gym.register( id="Isaac-Stack-Cube-Franka-IK-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs:FrankaCubeStackIKAbsMimicEnv", + entry_point=f"{__name__}.franka_stack_ik_abs_mimic_env:FrankaCubeStackIKAbsMimicEnv", kwargs={ - "env_cfg_entry_point": franka_stack_ik_abs_mimic_env_cfg.FrankaCubeStackIKAbsMimicEnvCfg, + "env_cfg_entry_point": f"{__name__}.franka_stack_ik_abs_mimic_env_cfg:FrankaCubeStackIKAbsMimicEnvCfg", }, disable_env_checker=True, ) gym.register( id="Isaac-Stack-Cube-Franka-IK-Rel-Visuomotor-Mimic-v0", - entry_point="isaaclab_mimic.envs:FrankaCubeStackIKRelMimicEnv", + entry_point=f"{__name__}.franka_stack_ik_rel_mimic_env:FrankaCubeStackIKRelMimicEnv", kwargs={ - "env_cfg_entry_point": franka_stack_ik_rel_visuomotor_mimic_env_cfg.FrankaCubeStackIKRelVisuomotorMimicEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.franka_stack_ik_rel_visuomotor_mimic_env_cfg:FrankaCubeStackIKRelVisuomotorMimicEnvCfg" + ), }, disable_env_checker=True, ) gym.register( id="Isaac-Stack-Cube-Franka-IK-Rel-Visuomotor-Cosmos-Mimic-v0", - entry_point="isaaclab_mimic.envs:FrankaCubeStackIKRelMimicEnv", + entry_point=f"{__name__}.franka_stack_ik_rel_mimic_env:FrankaCubeStackIKRelMimicEnv", kwargs={ - "env_cfg_entry_point": ( - franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg.FrankaCubeStackIKRelVisuomotorCosmosMimicEnvCfg - ), + "env_cfg_entry_point": f"{__name__}.franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg:FrankaCubeStackIKRelVisuomotorCosmosMimicEnvCfg", }, disable_env_checker=True, ) @@ -75,18 +67,18 @@ gym.register( id="Isaac-Stack-Cube-Franka-IK-Rel-Skillgen-v0", - entry_point="isaaclab_mimic.envs:FrankaCubeStackIKRelMimicEnv", + entry_point=f"{__name__}.franka_stack_ik_rel_mimic_env:FrankaCubeStackIKRelMimicEnv", kwargs={ - "env_cfg_entry_point": franka_stack_ik_rel_skillgen_env_cfg.FrankaCubeStackIKRelSkillgenEnvCfg, + "env_cfg_entry_point": f"{__name__}.franka_stack_ik_rel_skillgen_env_cfg:FrankaCubeStackIKRelSkillgenEnvCfg", }, disable_env_checker=True, ) gym.register( id="Isaac-Stack-Cube-Bin-Franka-IK-Rel-Mimic-v0", - entry_point="isaaclab_mimic.envs:FrankaCubeStackIKRelMimicEnv", + entry_point=f"{__name__}.franka_stack_ik_rel_mimic_env:FrankaCubeStackIKRelMimicEnv", kwargs={ - "env_cfg_entry_point": franka_bin_stack_ik_rel_mimic_env_cfg.FrankaBinStackIKRelMimicEnvCfg, + "env_cfg_entry_point": f"{__name__}.franka_bin_stack_ik_rel_mimic_env_cfg:FrankaBinStackIKRelMimicEnvCfg", }, disable_env_checker=True, ) diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py index 45e53110ab4e..76557802f463 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py index f3154c8f64f6..2bfadef874c3 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_bin_stack_ik_rel_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_bin_stack_ik_rel_mimic_env_cfg.py index ba40bd620e0f..ca28719d7306 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_bin_stack_ik_rel_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_bin_stack_ik_rel_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -16,7 +16,6 @@ class FrankaBinStackIKRelMimicEnvCfg(FrankaBinStackEnvCfg, MimicEnvCfg): """ def __post_init__(self): - # post init of parents super().__post_init__() diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env.py index ad113164c17e..45efc58bfc20 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env.py @@ -1,11 +1,12 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import torch from collections.abc import Sequence +import torch + import isaaclab.utils.math as PoseUtils from isaaclab.envs import ManagerBasedRLMimicEnv diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env_cfg.py index 39f68e111003..93e51d8f673e 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_abs_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_blueprint_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_blueprint_mimic_env_cfg.py index 1b8a8bd8d7ed..cd75bea018a1 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_blueprint_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_blueprint_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env.py index 6090161adcbb..336db05ca174 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env.py @@ -1,11 +1,12 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import torch from collections.abc import Sequence +import torch + import isaaclab.utils.math as PoseUtils from isaaclab.envs import ManagerBasedRLMimicEnv diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env_cfg.py index f9912c5e21d7..197852602d39 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_skillgen_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_skillgen_env_cfg.py index 714412cb5536..9d26039126be 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_skillgen_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_skillgen_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg.py index cfb1d54fe508..2f2ebe5ab339 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_mimic_env_cfg.py index 3f3b3bfd4a7d..ed63e973b2d0 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/franka_stack_ik_rel_visuomotor_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env.py index b92cd81d3ecc..b3d03a149317 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env_cfg.py index 83746beff687..d3de8a9aa3d2 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_abs_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -98,7 +98,8 @@ def __post_init__(self): subtask_term_offset_range=( 25, 30, - ), # this should be larger than the other subtasks, because the gripper should be lifted higher than 2 blocks + ), # This should be larger than the other subtasks, because the gripper + # should be lifted higher than two blocks # Selection strategy for source subtask segment selection_strategy="nearest_neighbor_object", # Optional parameters for the selection strategy function diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env.py index 953a6f536ced..c839cd655b13 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env_cfg.py index 77ba9c9f8d86..ce4d00015a3e 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/galbot_stack_rmp_rel_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -95,10 +95,9 @@ def __post_init__(self): # Corresponding key for the binary indicator in "datagen_info" for completion subtask_term_signal="grasp_2", # Time offsets for data generation when splitting a trajectory - subtask_term_offset_range=( - 25, - 30, - ), # this should be larger than the other subtasks, because the gripper should be lifted higher than 2 blocks + # This should be larger than the other subtasks, because the gripper + # should be lifted higher than two blocks + subtask_term_offset_range=(25, 30), # Selection strategy for source subtask segment selection_strategy="nearest_neighbor_object", # Optional parameters for the selection strategy function diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py index 9951c39cf2a7..1776528b7970 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py @@ -1,11 +1,12 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import torch from collections.abc import Sequence +import torch + import isaaclab.utils.math as PoseUtils from .franka_stack_ik_abs_mimic_env import FrankaCubeStackIKAbsMimicEnv diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/__init__.py index 7b6e491b6c6a..e7fe847c42e8 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -7,49 +7,43 @@ import gymnasium as gym -from .exhaustpipe_gr1t2_mimic_env_cfg import ExhaustPipeGR1T2MimicEnvCfg -from .locomanipulation_g1_mimic_env import LocomanipulationG1MimicEnv -from .locomanipulation_g1_mimic_env_cfg import LocomanipulationG1MimicEnvCfg -from .nutpour_gr1t2_mimic_env_cfg import NutPourGR1T2MimicEnvCfg -from .pickplace_gr1t2_mimic_env import PickPlaceGR1T2MimicEnv -from .pickplace_gr1t2_mimic_env_cfg import PickPlaceGR1T2MimicEnvCfg -from .pickplace_gr1t2_waist_enabled_mimic_env_cfg import PickPlaceGR1T2WaistEnabledMimicEnvCfg - gym.register( id="Isaac-PickPlace-GR1T2-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs.pinocchio_envs:PickPlaceGR1T2MimicEnv", + entry_point=f"{__name__}.pickplace_gr1t2_mimic_env:PickPlaceGR1T2MimicEnv", kwargs={ - "env_cfg_entry_point": pickplace_gr1t2_mimic_env_cfg.PickPlaceGR1T2MimicEnvCfg, + "env_cfg_entry_point": f"{__name__}.pickplace_gr1t2_mimic_env_cfg:PickPlaceGR1T2MimicEnvCfg", }, disable_env_checker=True, ) gym.register( id="Isaac-PickPlace-GR1T2-WaistEnabled-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs.pinocchio_envs:PickPlaceGR1T2MimicEnv", + entry_point=f"{__name__}.pickplace_gr1t2_mimic_env:PickPlaceGR1T2MimicEnv", kwargs={ - "env_cfg_entry_point": pickplace_gr1t2_waist_enabled_mimic_env_cfg.PickPlaceGR1T2WaistEnabledMimicEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.pickplace_gr1t2_waist_enabled_mimic_env_cfg:PickPlaceGR1T2WaistEnabledMimicEnvCfg" + ), }, disable_env_checker=True, ) gym.register( id="Isaac-NutPour-GR1T2-Pink-IK-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs.pinocchio_envs:PickPlaceGR1T2MimicEnv", - kwargs={"env_cfg_entry_point": nutpour_gr1t2_mimic_env_cfg.NutPourGR1T2MimicEnvCfg}, + entry_point=f"{__name__}.pickplace_gr1t2_mimic_env:PickPlaceGR1T2MimicEnv", + kwargs={"env_cfg_entry_point": f"{__name__}.nutpour_gr1t2_mimic_env_cfg:NutPourGR1T2MimicEnvCfg"}, disable_env_checker=True, ) gym.register( id="Isaac-ExhaustPipe-GR1T2-Pink-IK-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs.pinocchio_envs:PickPlaceGR1T2MimicEnv", - kwargs={"env_cfg_entry_point": exhaustpipe_gr1t2_mimic_env_cfg.ExhaustPipeGR1T2MimicEnvCfg}, + entry_point=f"{__name__}.pickplace_gr1t2_mimic_env:PickPlaceGR1T2MimicEnv", + kwargs={"env_cfg_entry_point": f"{__name__}.exhaustpipe_gr1t2_mimic_env_cfg:ExhaustPipeGR1T2MimicEnvCfg"}, disable_env_checker=True, ) gym.register( id="Isaac-Locomanipulation-G1-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs.pinocchio_envs:LocomanipulationG1MimicEnv", - kwargs={"env_cfg_entry_point": locomanipulation_g1_mimic_env_cfg.LocomanipulationG1MimicEnvCfg}, + entry_point=f"{__name__}.locomanipulation_g1_mimic_env:LocomanipulationG1MimicEnv", + kwargs={"env_cfg_entry_point": f"{__name__}.locomanipulation_g1_mimic_env_cfg:LocomanipulationG1MimicEnvCfg"}, disable_env_checker=True, ) diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/exhaustpipe_gr1t2_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/exhaustpipe_gr1t2_mimic_env_cfg.py index 83decc769f43..ed37975c6afe 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/exhaustpipe_gr1t2_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/exhaustpipe_gr1t2_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -13,6 +13,7 @@ @configclass class ExhaustPipeGR1T2MimicEnvCfg(ExhaustPipeGR1T2PinkIKEnvCfg, MimicEnvCfg): + """Configuration for GR1T2 Exhaust Pipe Mimic environment.""" def __post_init__(self): # Calling post init of parents diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env.py index ad612c61b0a6..89b13167315c 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env.py @@ -1,16 +1,18 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import torch from collections.abc import Sequence +import torch + import isaaclab.utils.math as PoseUtils from isaaclab.envs import ManagerBasedRLMimicEnv class LocomanipulationG1MimicEnv(ManagerBasedRLMimicEnv): + """G1 Locomanipulation Mimic environment.""" def get_robot_eef_pose(self, eef_name: str, env_ids: Sequence[int] | None = None) -> torch.Tensor: """ diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env_cfg.py index 150831a6ee84..2aa766dec33c 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/locomanipulation_g1_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -13,6 +13,7 @@ @configclass class LocomanipulationG1MimicEnvCfg(LocomanipulationG1EnvCfg, MimicEnvCfg): + """Configuration for G1 Locomanipulation Mimic environment.""" def __post_init__(self): # Call parent post-init diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/nutpour_gr1t2_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/nutpour_gr1t2_mimic_env_cfg.py index 2aa1b28864b2..683d4be09e44 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/nutpour_gr1t2_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/nutpour_gr1t2_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: Apache-2.0 @@ -11,6 +11,7 @@ @configclass class NutPourGR1T2MimicEnvCfg(NutPourGR1T2PinkIKEnvCfg, MimicEnvCfg): + """Configuration for GR1T2 Nut Pouring Mimic environment.""" def __post_init__(self): # Calling post init of parents diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env.py index 6bede8c0897c..9ec65040ef95 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env.py @@ -1,16 +1,18 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import torch from collections.abc import Sequence +import torch + import isaaclab.utils.math as PoseUtils from isaaclab.envs import ManagerBasedRLMimicEnv class PickPlaceGR1T2MimicEnv(ManagerBasedRLMimicEnv): + """GR1T2 Pick Place Mimic environment.""" def get_robot_eef_pose(self, eef_name: str, env_ids: Sequence[int] | None = None) -> torch.Tensor: """ diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env_cfg.py index 14c0ebd2a9d9..0297fb72a1bc 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -11,6 +11,7 @@ @configclass class PickPlaceGR1T2MimicEnvCfg(PickPlaceGR1T2EnvCfg, MimicEnvCfg): + """Configuration for GR1T2 Pick Place Mimic environment.""" def __post_init__(self): # Calling post init of parents diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_waist_enabled_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_waist_enabled_mimic_env_cfg.py index d5b033bf7371..f9528b277dba 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_waist_enabled_mimic_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pinocchio_envs/pickplace_gr1t2_waist_enabled_mimic_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -13,6 +13,7 @@ @configclass class PickPlaceGR1T2WaistEnabledMimicEnvCfg(PickPlaceGR1T2WaistEnabledEnvCfg, MimicEnvCfg): + """Configuration for GR1T2 Pick Place Waist Enabled Mimic environment.""" def __post_init__(self): # Calling post init of parents diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/__init__.py index 63333b6811e8..3af586c7dfb3 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/data_classes.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/data_classes.py index 2d2e656e288c..2ea2cf68b85e 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/data_classes.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/data_classes.py @@ -1,11 +1,12 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import torch from dataclasses import dataclass +import torch + @dataclass class LocomanipulationSDGInputData: @@ -35,7 +36,9 @@ class LocomanipulationSDGInputData: @dataclass class LocomanipulationSDGOutputData: - """A container for data that is recorded during locomanipulation replay. This is the final output of the pipeline""" + """A container for data that is recorded during locomanipulation replay. + This is the final output of the pipeline. + """ left_hand_pose_target: torch.Tensor | None = None """The left hand's target pose.""" diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/__init__.py index d73d89b0b06f..8d16a5ca14b8 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py index 1bd87096bfc4..dca2945822a3 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/g1_locomanipulation_sdg_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -36,7 +36,6 @@ @configclass class G1LocomanipulationSDGSceneCfg(LocomanipulationG1SceneCfg): - packing_table_2 = AssetBaseCfg( prim_path="/World/envs/env_.*/PackingTable2", init_state=AssetBaseCfg.InitialStateCfg( @@ -100,7 +99,6 @@ class G1LocomanipulationSDGObservationsCfg(ObservationsCfg): @configclass class PolicyCfg(ObservationsCfg.PolicyCfg): - robot_pov_cam = ObsTerm( func=manip_mdp.image, params={"sensor_cfg": SceneEntityCfg("robot_pov_cam"), "data_type": "rgb", "normalize": False}, @@ -134,14 +132,13 @@ def __post_init__(self): self.sim.render_interval = 6 # Set the URDF and mesh paths for the IK controller - urdf_omniverse_path = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_kinematics_asset/g1_29dof_with_hand_only_kinematics.urdf" + urdf_omniverse_path = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_kinematics_asset/g1_29dof_with_hand_only_kinematics.urdf" # noqa: E501 # Retrieve local paths for the URDF and mesh files. Will be cached for call after the first time. self.actions.upper_body_ik.controller.urdf_path = retrieve_file_path(urdf_omniverse_path) class G1LocomanipulationSDGEnv(LocomanipulationSDGEnv): - def __init__(self, cfg: G1LocomanipulationSDGEnvCfg, **kwargs): super().__init__(cfg) self.sim.set_camera_view([10.5, 10.5, 10.5], [0.0, 0.0, 0.5]) @@ -185,7 +182,6 @@ def build_action_vector( right_hand_joint_positions_target: torch.Tensor, base_velocity_target: torch.Tensor, ): - action = torch.zeros(self.action_space.shape) # Set base height @@ -193,15 +189,15 @@ def build_action_vector( action[0, lower_body_index_offset + 3 : lower_body_index_offset + 4] = torch.tensor([0.8]) # Left hand pose - assert left_hand_pose_target.shape == ( - self._frame_pose_dim, - ), f"Expected pose shape ({self._frame_pose_dim},), got {left_hand_pose_target.shape}" + assert left_hand_pose_target.shape == (self._frame_pose_dim,), ( + f"Expected pose shape ({self._frame_pose_dim},), got {left_hand_pose_target.shape}" + ) action[0, : self._frame_pose_dim] = left_hand_pose_target # Right hand pose - assert right_hand_pose_target.shape == ( - self._frame_pose_dim, - ), f"Expected pose shape ({self._frame_pose_dim},), got {right_hand_pose_target.shape}" + assert right_hand_pose_target.shape == (self._frame_pose_dim,), ( + f"Expected pose shape ({self._frame_pose_dim},), got {right_hand_pose_target.shape}" + ) action[0, self._frame_pose_dim : 2 * self._frame_pose_dim] = right_hand_pose_target # Left hand joint positions @@ -220,8 +216,7 @@ def build_action_vector( ) action[ 0, - 2 * self._frame_pose_dim - + self._number_of_finger_joints : 2 * self._frame_pose_dim + 2 * self._frame_pose_dim + self._number_of_finger_joints : 2 * self._frame_pose_dim + 2 * self._number_of_finger_joints, ] = right_hand_joint_positions_target @@ -261,7 +256,6 @@ def get_end_fixture(self) -> SceneFixture: ) def get_obstacle_fixtures(self): - obstacles = [ SceneFixture( self.scene, diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env.py index 6f9c095dac70..83ae8e656846 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -14,6 +14,7 @@ class LocomanipulationSDGOutputDataRecorder(RecorderTerm): + """Recorder for Locomanipulation SDG output data.""" def record_pre_step(self): output_data: LocomanipulationSDGOutputData = self._env._locomanipulation_sdg_output_data diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env_cfg.py index 77b82710026f..f3c5dd6c47cb 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/envs/locomanipulation_sdg_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/occupancy_map_utils.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/occupancy_map_utils.py index 077e6439238a..b2fdbdfb8527 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/occupancy_map_utils.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/occupancy_map_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -6,16 +6,17 @@ import enum import math -import numpy as np import os import tempfile -import torch -import yaml from dataclasses import dataclass import cv2 +import numpy as np import PIL.Image +import torch +import yaml from PIL import ImageDraw + from pxr import Kind, Sdf, Usd, UsdGeom, UsdShade @@ -39,7 +40,6 @@ class OccupancyMapDataValue(enum.IntEnum): OCCUPIED = 2 def ros_image_value(self, negate: bool = False) -> int: - values = [0, 127, 255] if negate: @@ -59,7 +59,6 @@ class OccupancyMapMergeMethod(enum.IntEnum): class OccupancyMap: - ROS_IMAGE_FILENAME = "map.png" ROS_YAML_FILENAME = "map.yaml" ROS_YAML_TEMPLATE = """ @@ -516,7 +515,6 @@ def _omap_world_to_px( width_pixels: int, height_pixels: int, ) -> np.ndarray: - bot_left_world = (origin[0], origin[1]) u = (points[:, 0] - bot_left_world[0]) / width_meters v = 1.0 - (points[:, 1] - bot_left_world[1]) / height_meters @@ -552,7 +550,6 @@ def merge_occupancy_maps( raise ValueError(f"Unsupported merge method: {method}") for src_omap in src_omaps: - omap_corners_in_world_coords = np.array( [src_omap.top_left_pixel_world_coords(), src_omap.bottom_right_pixel_world_coords()] ) @@ -622,12 +619,14 @@ def make_translate_transform(dx: float, dy: float) -> np.ndarray: def transform_occupancy_map(omap: OccupancyMap, transform: np.ndarray) -> OccupancyMap: """Transform an occupancy map using a 2D transform.""" - src_box_world_coords = np.array([ - [omap.origin[0], omap.origin[1]], - [omap.origin[0] + omap.width_meters(), omap.origin[1]], - [omap.origin[0] + omap.width_meters(), omap.origin[1] + omap.height_meters()], - [omap.origin[0], omap.origin[1] + omap.height_meters()], - ]) + src_box_world_coords = np.array( + [ + [omap.origin[0], omap.origin[1]], + [omap.origin[0] + omap.width_meters(), omap.origin[1]], + [omap.origin[0] + omap.width_meters(), omap.origin[1] + omap.height_meters()], + [omap.origin[0], omap.origin[1] + omap.height_meters()], + ] + ) src_box_pixel_coords = omap.world_to_pixel_numpy(src_box_world_coords) @@ -671,7 +670,6 @@ def occupancy_map_add_to_stage( draw_path: np.ndarray | torch.Tensor | None = None, draw_path_line_width_meter: float = 0.25, ) -> Usd.Prim: - image_path = os.path.join(tempfile.mkdtemp(), "texture.png") image = occupancy_map.ros_image() diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/path_utils.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/path_utils.py index d6a05d34bf45..f3e203f401e8 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/path_utils.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/path_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/scene_utils.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/scene_utils.py index 594b6daab0c9..dc9c969171c0 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/scene_utils.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/scene_utils.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import numpy as np import random + +import numpy as np import torch import isaaclab.utils.math as math_utils @@ -140,7 +141,6 @@ def __init__( self.occupancy_map_resolution = occupancy_map_resolution def get_occupancy_map(self): - local_occupancy_map = OccupancyMap.from_occupancy_boundary( boundary=self.occupancy_map_boundary, resolution=self.occupancy_map_resolution ) diff --git a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/transform_utils.py b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/transform_utils.py index 8f718bebd39e..73406a187ffd 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/transform_utils.py +++ b/source/isaaclab_mimic/isaaclab_mimic/locomanipulation_sdg/transform_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner.py b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner.py index f9c6cf69cbdb..a7e1ebb36252 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner.py +++ b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner.py @@ -1,14 +1,15 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 import logging -import numpy as np -import torch from dataclasses import dataclass from typing import Any +import numpy as np +import torch + from curobo.cuda_robot_model.cuda_robot_model import CudaRobotModelState from curobo.geom.sdf.world import CollisionCheckerType from curobo.geom.sphere_fit import SphereFitType diff --git a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner_cfg.py index 6755093f7046..b3b3f7cce827 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner_cfg.py +++ b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/curobo_planner_cfg.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 import os import tempfile + import yaml from curobo.geom.sdf.world import CollisionCheckerType @@ -77,7 +78,9 @@ class CuroboPlannerCfg: """Absolute USD prim path to the robot root for world extraction; None derives it from environment root.""" world_ignore_substrings: list[str] | None = None - """List of substring patterns to ignore when extracting world obstacles (e.g., default ground plane, debug prims).""" + """List of substring patterns to ignore when extracting world obstacles + (e.g., default ground plane, debug prims). + """ # Motion planning parameters collision_checker_type: CollisionCheckerType = CollisionCheckerType.MESH @@ -287,15 +290,19 @@ def my_robot_config(cls) -> "CuroboPlannerCfg": # Hand/finger links to disable during contact planning hand_link_names=["finger_link_1", "finger_link_2", "palm_link"], - # Optional: Absolute USD prim path to the robot root for world extraction; None derives it from environment root. + # Optional: Absolute USD prim path to the robot root for world extraction; + # None derives it from environment root. robot_prim_path=None, - # Optional: List of substring patterns to ignore when extracting world obstacles (e.g., default ground plane, debug prims). - # None derives it from the environment root and adds some default patterns. This is useful for environments with a lot of prims. + # Optional: List of substring patterns to ignore when extracting world obstacles + # (e.g., default ground plane, debug prims). + # None derives it from the environment root and adds some default patterns. + # This is useful for environments with a lot of prims. world_ignore_substrings=None, # Optional: Custom collision spheres configuration - collision_spheres_file="spheres/my_robot_spheres.yml", # Path relative to curobo (can override with custom spheres file) + # Path relative to curobo (can override with custom spheres file) + collision_spheres_file="spheres/my_robot_spheres.yml", # Grasp detection threshold grasp_gripper_open_val=0.05, diff --git a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/plan_visualizer.py b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/plan_visualizer.py index b9658a502894..0f5252e208c9 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/plan_visualizer.py +++ b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/curobo/plan_visualizer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -10,16 +10,17 @@ """ import atexit -import numpy as np import os import signal import subprocess import threading import time -import torch import weakref from typing import TYPE_CHECKING, Any, Optional +import numpy as np +import torch + # Check if rerun is installed try: import rerun as rr @@ -878,9 +879,7 @@ def _create_interpolated_trajectory(self, plan: JointState, interpolation_steps: """ if len(plan.position) < 2: # If only one waypoint, just return it - return [ - plan.position[0] if isinstance(plan.position[0], torch.Tensor) else torch.tensor(plan.position[0]) - ] # type: ignore + return [plan.position[0] if isinstance(plan.position[0], torch.Tensor) else torch.tensor(plan.position[0])] # type: ignore interpolated_positions = [] diff --git a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/motion_planner_base.py b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/motion_planner_base.py index 783363b73300..43bf6b140515 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/motion_planners/motion_planner_base.py +++ b/source/isaaclab_mimic/isaaclab_mimic/motion_planners/motion_planner_base.py @@ -1,12 +1,13 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 -import torch from abc import ABC, abstractmethod from typing import Any +import torch + from isaaclab.assets import Articulation from isaaclab.envs.manager_based_env import ManagerBasedEnv diff --git a/source/isaaclab_mimic/isaaclab_mimic/ui/instruction_display.py b/source/isaaclab_mimic/isaaclab_mimic/ui/instruction_display.py index ed2fb3c538ec..274038d3d9eb 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/ui/instruction_display.py +++ b/source/isaaclab_mimic/isaaclab_mimic/ui/instruction_display.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/setup.py b/source/isaaclab_mimic/setup.py index 95e4c2933f21..c43d268fe260 100644 --- a/source/isaaclab_mimic/setup.py +++ b/source/isaaclab_mimic/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -8,8 +8,8 @@ import itertools import os import platform -import toml +import toml from setuptools import setup # Obtain the extension data from the extension.toml file diff --git a/source/isaaclab_mimic/test/test_curobo_planner_cube_stack.py b/source/isaaclab_mimic/test/test_curobo_planner_cube_stack.py index 844db6fafd5f..4c532f62ef62 100644 --- a/source/isaaclab_mimic/test/test_curobo_planner_cube_stack.py +++ b/source/isaaclab_mimic/test/test_curobo_planner_cube_stack.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -23,9 +23,10 @@ app_launcher = AppLauncher(headless=headless) simulation_app: Any = app_launcher.app +from collections.abc import Generator + import gymnasium as gym import torch -from collections.abc import Generator import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation, RigidObject @@ -204,9 +205,9 @@ def test_pick_and_stack(self) -> None: self._visualize_goal_pose(pos_place, quat_place) # Plan with attached object - assert self.planner.update_world_and_plan_motion( - place_pose, expected_attached_object="cube_1" - ), "Failed to plan placement trajectory with attached cube" + assert self.planner.update_world_and_plan_motion(place_pose, expected_attached_object="cube_1"), ( + "Failed to plan placement trajectory with attached cube" + ) _execute_plan(self.env, self.planner, gripper_binary_action=GRIPPER_CLOSE_CMD) # Release cube 1 @@ -221,9 +222,9 @@ def test_pick_and_stack(self) -> None: quat_pg = math_utils.quat_from_matrix(pre_grasp_pose[:3, :3].unsqueeze(0))[0].detach().cpu() self._visualize_goal_pose(pos_pg, quat_pg) - assert self.planner.update_world_and_plan_motion( - pre_grasp_pose, expected_attached_object=None - ), "Failed to plan retract motion" + assert self.planner.update_world_and_plan_motion(pre_grasp_pose, expected_attached_object=None), ( + "Failed to plan retract motion" + ) _execute_plan(self.env, self.planner, gripper_binary_action=GRIPPER_OPEN_CMD) # Grasp cube 3 @@ -237,9 +238,9 @@ def test_pick_and_stack(self) -> None: quat_place = math_utils.quat_from_matrix(place_pose[:3, :3].unsqueeze(0))[0].detach().cpu() self._visualize_goal_pose(pos_place, quat_place) - assert self.planner.update_world_and_plan_motion( - place_pose, expected_attached_object="cube_3" - ), "Failed to plan placement trajectory with attached cube" + assert self.planner.update_world_and_plan_motion(place_pose, expected_attached_object="cube_3"), ( + "Failed to plan placement trajectory with attached cube" + ) _execute_plan(self.env, self.planner, gripper_binary_action=GRIPPER_CLOSE_CMD) # Release cube 3 diff --git a/source/isaaclab_mimic/test/test_curobo_planner_franka.py b/source/isaaclab_mimic/test/test_curobo_planner_franka.py index 323caf99c284..9e5adf724c2f 100644 --- a/source/isaaclab_mimic/test/test_curobo_planner_franka.py +++ b/source/isaaclab_mimic/test/test_curobo_planner_franka.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/test/test_generate_dataset.py b/source/isaaclab_mimic/test/test_generate_dataset.py index 9125c8e8619d..8568ab4ec01d 100644 --- a/source/isaaclab_mimic/test/test_generate_dataset.py +++ b/source/isaaclab_mimic/test/test_generate_dataset.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -91,9 +91,9 @@ def setup_test_environment(): # Extract the number from the line try: successful_count = int(success_line.split(":")[-1].strip()) - assert ( - successful_count == EXPECTED_SUCCESSFUL_ANNOTATIONS - ), f"Expected 10 successful annotations but got {successful_count}" + assert successful_count == EXPECTED_SUCCESSFUL_ANNOTATIONS, ( + f"Expected 10 successful annotations but got {successful_count}" + ) except (ValueError, IndexError) as e: pytest.fail(f"Could not parse successful task count from line: '{success_line}'. Error: {e}") diff --git a/source/isaaclab_mimic/test/test_generate_dataset_skillgen.py b/source/isaaclab_mimic/test/test_generate_dataset_skillgen.py index 846604a1c0c2..7f5afc7d664c 100644 --- a/source/isaaclab_mimic/test/test_generate_dataset_skillgen.py +++ b/source/isaaclab_mimic/test/test_generate_dataset_skillgen.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 diff --git a/source/isaaclab_mimic/test/test_selection_strategy.py b/source/isaaclab_mimic/test/test_selection_strategy.py index cc5b8a1bea8e..ac58be34db0f 100644 --- a/source/isaaclab_mimic/test/test_selection_strategy.py +++ b/source/isaaclab_mimic/test/test_selection_strategy.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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: Apache-2.0 @@ -9,9 +9,8 @@ simulation_app = AppLauncher(headless=True).app import numpy as np -import torch - import pytest +import torch import isaaclab.utils.math as PoseUtils @@ -104,9 +103,9 @@ def test_select_source_demo_identity_orientations_object_strategy(nearest_neighb ] # Assert that all selected indices are valid indices within cluster 1 - assert np.all( - np.array(selected_indices) < len(src_object_poses_in_world_cluster_1) - ), "Some selected indices are not part of cluster 1." + assert np.all(np.array(selected_indices) < len(src_object_poses_in_world_cluster_1)), ( + "Some selected indices are not part of cluster 1." + ) # Test 2: # Set the current object pose to the first value of cluster 2 and add some noise @@ -136,12 +135,12 @@ def test_select_source_demo_identity_orientations_object_strategy(nearest_neighb ] # Assert that all selected indices are valid indices within cluster 2 - assert np.all( - np.array(selected_indices) < len(src_object_poses_in_world) - ), "Some selected indices are not part of cluster 2." - assert np.all( - np.array(selected_indices) > (len(src_object_poses_in_world_cluster_1) - 1) - ), "Some selected indices are not part of cluster 2." + assert np.all(np.array(selected_indices) < len(src_object_poses_in_world)), ( + "Some selected indices are not part of cluster 2." + ) + assert np.all(np.array(selected_indices) > (len(src_object_poses_in_world_cluster_1) - 1)), ( + "Some selected indices are not part of cluster 2." + ) def test_select_source_demo_identity_orientations_robot_distance_strategy(nearest_neighbor_robot_distance_strategy): @@ -176,10 +175,12 @@ def test_select_source_demo_identity_orientations_robot_distance_strategy(neares transformed_eef_in_world_poses_tensor = torch.stack(transformed_eef_pose_cluster_1 + transformed_eef_pose_cluster_2) # Create transformation matrices corresponding to each source object pose - src_obj_in_world_poses = torch.stack([ - PoseUtils.generate_random_transformation_matrix(pos_boundary=10, rot_boundary=(2 * np.pi)) - for _ in range(transformed_eef_in_world_poses_tensor.shape[0]) - ]) + src_obj_in_world_poses = torch.stack( + [ + PoseUtils.generate_random_transformation_matrix(pos_boundary=10, rot_boundary=(2 * np.pi)) + for _ in range(transformed_eef_in_world_poses_tensor.shape[0]) + ] + ) # Calculate the src_eef poses from the transformed eef poses, src_obj_in_world and curr_obj_pose_in_world # This is the inverse of the transformation of the eef pose done in NearestNeighborRobotDistanceStrategy @@ -238,9 +239,9 @@ def test_select_source_demo_identity_orientations_robot_distance_strategy(neares ] # Assert that all selected indices are valid indices within cluster 1 - assert np.all( - np.array(selected_indices) < len(transformed_eef_pose_cluster_1) - ), "Some selected indices are not part of cluster 1." + assert np.all(np.array(selected_indices) < len(transformed_eef_pose_cluster_1)), ( + "Some selected indices are not part of cluster 1." + ) # Test 2: Ensure the nearest neighbor is always part of cluster 2 max_deviation = 3 # Define a maximum deviation for the current pose @@ -269,9 +270,9 @@ def test_select_source_demo_identity_orientations_robot_distance_strategy(neares ] # Assert that all selected indices are valid indices within cluster 2 - assert np.all( - np.array(selected_indices) < transformed_eef_in_world_poses_tensor.shape[0] - ), "Some selected indices are not part of cluster 2." - assert np.all( - np.array(selected_indices) > (len(transformed_eef_pose_cluster_1) - 1) - ), "Some selected indices are not part of cluster 2." + assert np.all(np.array(selected_indices) < transformed_eef_in_world_poses_tensor.shape[0]), ( + "Some selected indices are not part of cluster 2." + ) + assert np.all(np.array(selected_indices) > (len(transformed_eef_pose_cluster_1) - 1)), ( + "Some selected indices are not part of cluster 2." + ) diff --git a/source/isaaclab_rl/config/extension.toml b/source/isaaclab_rl/config/extension.toml index 0e2f31470b63..79f171d3398c 100644 --- a/source/isaaclab_rl/config/extension.toml +++ b/source/isaaclab_rl/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.4.4" +version = "0.4.7" # Description title = "Isaac Lab RL" diff --git a/source/isaaclab_rl/docs/CHANGELOG.rst b/source/isaaclab_rl/docs/CHANGELOG.rst index e3d44a08d967..6fe0be78d0a5 100644 --- a/source/isaaclab_rl/docs/CHANGELOG.rst +++ b/source/isaaclab_rl/docs/CHANGELOG.rst @@ -1,6 +1,35 @@ Changelog --------- +0.4.7 (2025-12-29) +~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Added :mod:`isaaclab_rl.utils.pretrained_checkpoint` sub-module to handle various pre-trained checkpoint tasks. + This module was previously located in the :mod:`isaaclab.utils` module. + + +0.4.6 (2025-11-10) +~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Added support for decoupling RL device from simulation device in for RL games wrapper. + This allows users to run simulation on one device (e.g., CPU) while running RL training/inference on another device. + + +0.4.5 (2025-12-01) +~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added state_dependent_std rsl_rl param to RSL-RL wrapper. + + 0.4.4 (2025-10-15) ~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab_rl/isaaclab_rl/__init__.py b/source/isaaclab_rl/isaaclab_rl/__init__.py index 804f7ad5a561..f9e67f543a74 100644 --- a/source/isaaclab_rl/isaaclab_rl/__init__.py +++ b/source/isaaclab_rl/isaaclab_rl/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py b/source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py index 38bfa1f4ec33..f1d006ae6cc9 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py index 5eab19288f0c..c56bf4f40e51 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py index bd6f04be0938..ad942de8eec9 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py index 714d5eea1838..aeec36055eb1 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py @@ -1,15 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np import os import random import sys + +import numpy as np import torch import torch.distributed as dist - from rl_games.common.algo_observer import AlgoObserver from . import pbt_utils diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py index 63cc534edd6a..b494dd1fdefe 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py index 2ce88010af58..959c24e41031 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +7,11 @@ import os import random import socket -import yaml from collections import OrderedDict from pathlib import Path -from prettytable import PrettyTable +import yaml +from prettytable import PrettyTable from rl_games.algos_torch.torch_ext import safe_filesystem_op, safe_save @@ -274,16 +274,18 @@ def print_ckpt_summary(self, sumry: dict[int, dict | None]): if c is None: t.add_row([p, "—", "", "", "", "", "", ""]) else: - t.add_row([ - p, - "OK", - self.fmt(c.get("true_objective", "")), - c.get("iteration", ""), - c.get("frame", ""), - c.get("experiment_name", ""), - self.short(c.get("checkpoint", "")), - self.short(c.get("pbt_checkpoint", "")), - ]) + t.add_row( + [ + p, + "OK", + self.fmt(c.get("true_objective", "")), + c.get("iteration", ""), + c.get("frame", ""), + c.get("experiment_name", ""), + self.short(c.get("checkpoint", "")), + self.short(c.get("pbt_checkpoint", "")), + ] + ) print(t) def print_mutation_diff(self, before: dict, after: dict, *, header: str = "Mutated params (changed only)"): diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/rl_games.py b/source/isaaclab_rl/isaaclab_rl/rl_games/rl_games.py index 8c448c172ac4..d5c786c7c9e7 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/rl_games.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/rl_games.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -34,11 +34,11 @@ # needed to import for allowing type-hinting:gym.spaces.Box | None from __future__ import annotations +from collections.abc import Callable + import gym.spaces # needed for rl-games incompatibility: https://github.com/Denys88/rl_games/issues/261 import gymnasium import torch -from collections.abc import Callable - from rl_games.common import env_configurations from rl_games.common.vecenv import IVecEnv @@ -319,6 +319,10 @@ def _process_obs(self, obs_dict: VecEnvObs) -> dict[str, torch.Tensor] | dict[st - ``"obs"``: either a concatenated tensor (``concate_obs_group=True``) or a Dict of group tensors. - ``"states"`` (optional): same structure as above when state groups are configured; omitted otherwise. """ + # move observations to RL device if different from sim device + if self._rl_device != self._sim_device: + obs_dict = {key: obs.to(device=self._rl_device) for key, obs in obs_dict.items()} + # clip the observations for key, obs in obs_dict.items(): obs_dict[key] = torch.clamp(obs, -self._clip_obs, self._clip_obs) diff --git a/source/isaaclab_rl/isaaclab_rl/rsl_rl/__init__.py b/source/isaaclab_rl/isaaclab_rl/rsl_rl/__init__.py index 74f26d196ede..3d8c62968994 100644 --- a/source/isaaclab_rl/isaaclab_rl/rsl_rl/__init__.py +++ b/source/isaaclab_rl/isaaclab_rl/rsl_rl/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rsl_rl/distillation_cfg.py b/source/isaaclab_rl/isaaclab_rl/rsl_rl/distillation_cfg.py index 7cdcbfe0c5e6..1a631eeffdaa 100644 --- a/source/isaaclab_rl/isaaclab_rl/rsl_rl/distillation_cfg.py +++ b/source/isaaclab_rl/isaaclab_rl/rsl_rl/distillation_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rsl_rl/exporter.py b/source/isaaclab_rl/isaaclab_rl/rsl_rl/exporter.py index fc3023557414..d5b8a248adb8 100644 --- a/source/isaaclab_rl/isaaclab_rl/rsl_rl/exporter.py +++ b/source/isaaclab_rl/isaaclab_rl/rsl_rl/exporter.py @@ -1,10 +1,11 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 copy import os + import torch diff --git a/source/isaaclab_rl/isaaclab_rl/rsl_rl/rl_cfg.py b/source/isaaclab_rl/isaaclab_rl/rsl_rl/rl_cfg.py index 5b03a7c639b4..7be991174dec 100644 --- a/source/isaaclab_rl/isaaclab_rl/rsl_rl/rl_cfg.py +++ b/source/isaaclab_rl/isaaclab_rl/rsl_rl/rl_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +31,9 @@ class RslRlPpoActorCriticCfg: noise_std_type: Literal["scalar", "log"] = "scalar" """The type of noise standard deviation for the policy. Default is scalar.""" + state_dependent_std: bool = False + """Whether to use state-dependent standard deviation for the policy. Default is False.""" + actor_obs_normalization: bool = MISSING """Whether to normalize the observation for the actor network.""" diff --git a/source/isaaclab_rl/isaaclab_rl/rsl_rl/rnd_cfg.py b/source/isaaclab_rl/isaaclab_rl/rsl_rl/rnd_cfg.py index fbc68dec3b85..0cc698dc8813 100644 --- a/source/isaaclab_rl/isaaclab_rl/rsl_rl/rnd_cfg.py +++ b/source/isaaclab_rl/isaaclab_rl/rsl_rl/rnd_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rsl_rl/symmetry_cfg.py b/source/isaaclab_rl/isaaclab_rl/rsl_rl/symmetry_cfg.py index 0cd476e848db..8f60c7430686 100644 --- a/source/isaaclab_rl/isaaclab_rl/rsl_rl/symmetry_cfg.py +++ b/source/isaaclab_rl/isaaclab_rl/rsl_rl/symmetry_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_rl/isaaclab_rl/rsl_rl/vecenv_wrapper.py b/source/isaaclab_rl/isaaclab_rl/rsl_rl/vecenv_wrapper.py index 73ceae04693b..dde20f2bb165 100644 --- a/source/isaaclab_rl/isaaclab_rl/rsl_rl/vecenv_wrapper.py +++ b/source/isaaclab_rl/isaaclab_rl/rsl_rl/vecenv_wrapper.py @@ -1,13 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 gymnasium as gym import torch -from tensordict import TensorDict - from rsl_rl.env import VecEnv +from tensordict import TensorDict from isaaclab.envs import DirectRLEnv, ManagerBasedRLEnv diff --git a/source/isaaclab_rl/isaaclab_rl/sb3.py b/source/isaaclab_rl/isaaclab_rl/sb3.py index 6513cfc1c12c..2177bc6252c4 100644 --- a/source/isaaclab_rl/isaaclab_rl/sb3.py +++ b/source/isaaclab_rl/isaaclab_rl/sb3.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,13 +18,13 @@ # needed to import for allowing type-hinting: torch.Tensor | dict[str, torch.Tensor] from __future__ import annotations +import warnings +from typing import Any + import gymnasium as gym import numpy as np import torch import torch.nn as nn # noqa: F401 -import warnings -from typing import Any - from stable_baselines3.common.preprocessing import is_image_space, is_image_space_channels_first from stable_baselines3.common.utils import constant_fn from stable_baselines3.common.vec_env.base_vec_env import VecEnv, VecEnvObs, VecEnvStepReturn diff --git a/source/isaaclab_rl/isaaclab_rl/skrl.py b/source/isaaclab_rl/isaaclab_rl/skrl.py index 3e5661dedd49..5aba121523f2 100644 --- a/source/isaaclab_rl/isaaclab_rl/skrl.py +++ b/source/isaaclab_rl/isaaclab_rl/skrl.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -18,7 +18,7 @@ .. code-block:: python from skrl.envs.torch.wrappers import wrap_env # for PyTorch, or... - from skrl.envs.jax.wrappers import wrap_env # for JAX + from skrl.envs.jax.wrappers import wrap_env # for JAX env = wrap_env(env, wrapper="isaaclab") diff --git a/source/isaaclab_rl/isaaclab_rl/utils/__init__.py b/source/isaaclab_rl/isaaclab_rl/utils/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab_rl/isaaclab_rl/utils/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab/isaaclab/utils/pretrained_checkpoint.py b/source/isaaclab_rl/isaaclab_rl/utils/pretrained_checkpoint.py similarity index 96% rename from source/isaaclab/isaaclab/utils/pretrained_checkpoint.py rename to source/isaaclab_rl/isaaclab_rl/utils/pretrained_checkpoint.py index 0069b6c453d8..c2ada0e9b5e8 100644 --- a/source/isaaclab/isaaclab/utils/pretrained_checkpoint.py +++ b/source/isaaclab_rl/isaaclab_rl/utils/pretrained_checkpoint.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,17 +11,18 @@ import carb.settings -from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR, retrieve_file_path from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry # noqa: F401 -from .assets import retrieve_file_path - PRETRAINED_CHECKPOINTS_ASSET_ROOT_DIR = carb.settings.get_settings().get( "/persistent/isaaclab/asset_root/pretrained_checkpoints" ) """Path to the root directory on the Nucleus Server.""" +PRETRAINED_CHECKPOINT_PATH = str(PRETRAINED_CHECKPOINTS_ASSET_ROOT_DIR) + "/Isaac/IsaacLab/PretrainedCheckpoints" +"""URL for where we store all the pre-trained checkpoints""" + WORKFLOWS = ["rl_games", "rsl_rl", "sb3", "skrl"] """The supported workflows for pre-trained checkpoints""" @@ -31,24 +32,21 @@ WORKFLOW_PLAYER = {w: f"scripts/reinforcement_learning/{w}/play.py" for w in WORKFLOWS} """A dict mapping workflow to their play program path""" -PRETRAINED_CHECKPOINT_PATH = str(PRETRAINED_CHECKPOINTS_ASSET_ROOT_DIR) + "/Isaac/IsaacLab/PretrainedCheckpoints" -"""URL for where we store all the pre-trained checkpoints""" - -"""The filename for checkpoints used by the different workflows""" WORKFLOW_PRETRAINED_CHECKPOINT_FILENAMES = { "rl_games": "checkpoint.pth", "rsl_rl": "checkpoint.pt", "sb3": "checkpoint.zip", "skrl": "checkpoint.pt", } +"""The filename for checkpoints used by the different workflows""" -"""Maps workflow to the agent variable name that determines the logging directory logs/{workflow}/{variable}""" WORKFLOW_EXPERIMENT_NAME_VARIABLE = { "rl_games": "agent.params.config.name", "rsl_rl": "agent.experiment_name", "sb3": None, "skrl": "agent.agent.experiment.directory", } +"""Maps workflow to the agent variable name that determines the logging directory logs/{workflow}/{variable}""" def has_pretrained_checkpoints_asset_root_dir() -> bool: @@ -61,7 +59,7 @@ def get_log_root_path(workflow: str, task_name: str) -> str: return os.path.abspath(os.path.join("logs", workflow, task_name)) -def get_latest_job_run_path(workflow: str, task_name: str) -> str: +def get_latest_job_run_path(workflow: str, task_name: str) -> str | None: """The local logs path of the most recent run of this workflow and task name""" log_root_path = get_log_root_path(workflow, task_name) return _get_latest_file_or_directory(log_root_path) @@ -77,7 +75,7 @@ def get_pretrained_checkpoint_path(workflow: str, task_name: str) -> str: if workflow == "rl_games": return os.path.join(path, "nn", f"{task_name}.pth") elif workflow == "rsl_rl": - return _get_latest_file_or_directory(path, "*.pt") + return _get_latest_file_or_directory(path, "*.pt") # type: ignore elif workflow == "sb3": return os.path.join(path, "model.zip") elif workflow == "skrl": diff --git a/source/isaaclab_rl/setup.py b/source/isaaclab_rl/setup.py index 173c8257c733..998fba147a2a 100644 --- a/source/isaaclab_rl/setup.py +++ b/source/isaaclab_rl/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +7,8 @@ import itertools import os -import toml +import toml from setuptools import setup # Obtain the extension data from the extension.toml file @@ -46,7 +46,7 @@ "rl-games @ git+https://github.com/isaac-sim/rl_games.git@python3.11", "gym", ], # rl-games still needs gym :( - "rsl-rl": ["rsl-rl-lib==3.0.1", "onnxscript>=0.5"], # linux aarch 64 requires manual onnxscript installation + "rsl-rl": ["rsl-rl-lib==3.1.2", "onnxscript>=0.5"], # linux aarch 64 requires manual onnxscript installation } # Add the names with hyphens as aliases for convenience EXTRAS_REQUIRE["rl_games"] = EXTRAS_REQUIRE["rl-games"] diff --git a/source/isaaclab_rl/test/test_rl_games_wrapper.py b/source/isaaclab_rl/test/test_rl_games_wrapper.py index 95a183ad0c25..e7f01a561b99 100644 --- a/source/isaaclab_rl/test/test_rl_games_wrapper.py +++ b/source/isaaclab_rl/test/test_rl_games_wrapper.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,13 +14,14 @@ """Rest everything follows.""" -import gymnasium as gym import os + +import gymnasium as gym +import pytest import torch import carb import omni.usd -import pytest from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent diff --git a/source/isaaclab_rl/test/test_rsl_rl_wrapper.py b/source/isaaclab_rl/test/test_rsl_rl_wrapper.py index 4eaf921be85c..2ae427df2797 100644 --- a/source/isaaclab_rl/test/test_rsl_rl_wrapper.py +++ b/source/isaaclab_rl/test/test_rsl_rl_wrapper.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,12 +15,12 @@ """Rest everything follows.""" import gymnasium as gym +import pytest import torch from tensordict import TensorDict import carb import omni.usd -import pytest from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent diff --git a/source/isaaclab_rl/test/test_sb3_wrapper.py b/source/isaaclab_rl/test/test_sb3_wrapper.py index 6fd63eaa73e5..be5dc46741e7 100644 --- a/source/isaaclab_rl/test/test_sb3_wrapper.py +++ b/source/isaaclab_rl/test/test_sb3_wrapper.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,11 +16,11 @@ import gymnasium as gym import numpy as np +import pytest import torch import carb import omni.usd -import pytest from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent diff --git a/source/isaaclab_rl/test/test_skrl_wrapper.py b/source/isaaclab_rl/test/test_skrl_wrapper.py index ae83058ff446..25ce35a1c476 100644 --- a/source/isaaclab_rl/test/test_skrl_wrapper.py +++ b/source/isaaclab_rl/test/test_skrl_wrapper.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +15,11 @@ """Rest everything follows.""" import gymnasium as gym +import pytest import torch import carb import omni.usd -import pytest from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent diff --git a/source/isaaclab_tasks/config/extension.toml b/source/isaaclab_tasks/config/extension.toml index 89b8c2c0e0e9..ea211a93e2a3 100644 --- a/source/isaaclab_tasks/config/extension.toml +++ b/source/isaaclab_tasks/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.11.6" +version = "0.11.12" # Description title = "Isaac Lab Environments" diff --git a/source/isaaclab_tasks/docs/CHANGELOG.rst b/source/isaaclab_tasks/docs/CHANGELOG.rst index e216caab37ae..f9100882c5d5 100644 --- a/source/isaaclab_tasks/docs/CHANGELOG.rst +++ b/source/isaaclab_tasks/docs/CHANGELOG.rst @@ -1,6 +1,78 @@ Changelog --------- +0.11.12 (2025-12-16) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added ``Isaac-Deploy-GearAssembly`` environments. + + +0.11.11 (2025-12-16) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added reaching task environments for OpenArm unimanual robot: + * :class:`OpenArmReachEnvCfg`; Gym ID ``Isaac-Reach-OpenArm-v0``. + * :class:`OpenArmReachEnvCfg_PLAY`; Gym ID ``Isaac-Reach-OpenArm-Play-v0``. +* Added lifting a cube task environments for OpenArm unimanual robot: + * :class:`OpenArmCubeLiftEnvCfg`; Gym ID ``Isaac-Lift-Cube-OpenArm-v0``. + * :class:`OpenArmCubeLiftEnvCfg_PLAY`; Gym ID ``Isaac-Lift-Cube-OpenArm-Play-v0``. +* Added opening a drawer task environments for OpenArm unimanual robot: + * :class:`OpenArmCabinetEnvCfg`; Gym ID ``Isaac-Open-Drawer-OpenArm-v0``. + * :class:`OpenArmCabinetEnvCfg_PLAY`; Gym ID ``Isaac-Open-Drawer-OpenArm-Play-v0``. +* Added reaching task environments for OpenArm bimanual robot: + * :class:`OpenArmReachEnvCfg`; Gym ID ``Isaac-Reach-OpenArm-Bi-v0``. + * :class:`OpenArmReachEnvCfg_PLAY`; Gym ID ``Isaac-Reach-OpenArm-Bi-Play-v0``. + + +0.11.10 (2025-12-13) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added obs_groups to the RSL-RL PPO agent configuration for the ``Isaac-Reach-UR10e-v0`` environment. +* Changed self.state_space to 19 in the ``Isaac-Reach-UR10e-ROS-Inference-v0`` environment. + + +0.11.9 (2025-11-10) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added OpenXR motion controller support for the G1 robot locomanipulation environment + ``Isaac-PickPlace-Locomanipulation-G1-Abs-v0``. This enables teleoperation using XR motion controllers + in addition to hand tracking. +* Added :class:`OpenXRDeviceMotionController` for motion controller-based teleoperation with headset anchoring control. +* Added motion controller-specific retargeters: + * :class:`G1TriHandControllerUpperBodyRetargeterCfg` for upper body and hand control using motion controllers. + * :class:`G1LowerBodyStandingControllerRetargeterCfg` for lower body control using motion controllers. + + +0.11.8 (2025-11-06) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Changed to use of ``num_rerenders_on_reset`` and ``DLAA`` in visuomotor imitation learning environments. + + +0.11.7 (2025-10-22) +~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Ensured all imports follows the string import style instead of direct import of environment. + + 0.11.6 (2025-10-23) ~~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab_tasks/isaaclab_tasks/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/__init__.py index 16871efcb911..60b7f852ef26 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -34,6 +34,6 @@ # The blacklist is used to prevent importing configs from sub-packages # TODO(@ashwinvk): Remove pick_place from the blacklist once pinocchio from Isaac Sim is compatibility -_BLACKLIST_PKGS = ["utils", ".mdp", "pick_place"] +_BLACKLIST_PKGS = ["utils", ".mdp", "pick_place", "direct.humanoid_amp.motions"] # Import all configs in this package import_packages(__name__, _BLACKLIST_PKGS) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/__init__.py index b7ae2178f6a1..3e2b7945ebde 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/__init__.py index 79e3fa55e481..636ad3661659 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rl_games_ppo_cfg.yaml index 418acdcda0bf..36d441d26dd8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rsl_rl_ppo_cfg.py index 8da27d1a7e00..871250fd0b17 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/skrl_ppo_cfg.yaml index 42917104e36d..de44a576eb98 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py index 06b470ca61f9..75087bbe0199 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/allegro_hand/allegro_hand_env_cfg.py @@ -1,11 +1,9 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG - import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, RigidObjectCfg from isaaclab.envs import DirectRLEnvCfg @@ -16,6 +14,8 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG + @configclass class AllegroHandEnvCfg(DirectRLEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/ant/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/__init__.py index 5f66eda9885a..9881cd66ca74 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/ant/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rl_games_ppo_cfg.yaml index 917f0841c7b7..a8c24a530962 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rsl_rl_ppo_cfg.py index 5ea9520fec2c..00eefc843e20 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/skrl_ppo_cfg.yaml index 78dcc9de5d1d..bb7382d4a2b8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/ant/ant_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/ant_env.py index c63b42acb384..39ae57b29677 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/ant/ant_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/ant/ant_env.py @@ -1,12 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -from isaaclab_assets.robots.ant import ANT_CFG - import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg from isaaclab.envs import DirectRLEnvCfg @@ -17,6 +15,8 @@ from isaaclab_tasks.direct.locomotion.locomotion_env import LocomotionEnv +from isaaclab_assets.robots.ant import ANT_CFG + @configclass class AntEnvCfg(DirectRLEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/__init__.py index 6c7759c049cd..26275c97fff9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_flat_ppo_cfg.yaml index 316145bca38e..6e8fc0d4ca91 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_rough_ppo_cfg.yaml index 0d25434634c7..ef2670326e20 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rl_games_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rsl_rl_ppo_cfg.py index efdf7d4f991a..117ad6e75bed 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_flat_ppo_cfg.yaml index 693ca6c2b306..33bc471a4778 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_rough_ppo_cfg.yaml index f235de692af0..c31294b7fa86 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env.py index d78724ca7902..2f7e792f93ad 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env_cfg.py index 70f9b53560b7..f5e12b599122 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/anymal_c/anymal_c_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/__init__.py index 27b9e5fecdf0..9da64598c0c4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/rl_games_ppo_cfg.yaml index fe87007f4acc..3d8d070248cf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env.py index 678035f0b0f0..35e999120380 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env.py @@ -1,16 +1,17 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import json -import numpy as np import os + +import numpy as np import torch +import warp as wp import carb import isaacsim.core.utils.torch as torch_utils -import warp as wp import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, RigidObject @@ -31,7 +32,6 @@ class AssemblyEnv(DirectRLEnv): cfg: AssemblyEnvCfg def __init__(self, cfg: AssemblyEnvCfg, render_mode: str | None = None, **kwargs): - # Update number of obs/states cfg.observation_space = sum([OBS_DIM_CFG[obs] for obs in cfg.obs_order]) cfg.state_space = sum([STATE_DIM_CFG[state] for state in cfg.state_order]) @@ -71,7 +71,6 @@ def __init__(self, cfg: AssemblyEnvCfg, render_mode: str | None = None, **kwargs self._init_eval_logging() def _init_eval_logging(self): - self.held_asset_pose_log = torch.empty( (0, 7), dtype=torch.float32, device=self.device ) # (position, quaternion) @@ -552,7 +551,6 @@ def _get_rewards(self): rew_buf *= sbc_rwd_scale if self.cfg_task.if_sbc: - self.curr_max_disp = automate_algo.get_new_max_disp( curr_success=torch.count_nonzero(self.ep_succeeded) / self.num_envs, cfg_task=self.cfg_task, @@ -750,7 +748,6 @@ def step_sim_no_action(self): self._compute_intermediate_values(dt=self.physics_dt) def randomize_fixed_initial_state(self, env_ids): - # (1.) Randomize fixed asset pose. fixed_state = self._fixed_asset.data.default_root_state.clone()[env_ids] # (1.a.) Position @@ -788,7 +785,6 @@ def randomize_fixed_initial_state(self, env_ids): self.step_sim_no_action() def randomize_held_initial_state(self, env_ids, pre_grasp): - curr_curriculum_disp_range = self.curriculum_height_bound[:, 1] - self.curr_max_disp if pre_grasp: self.curriculum_disp = self.curr_max_disp + curr_curriculum_disp_range * ( diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env_cfg.py index 78cb3d3da6af..5d4f66ec8969 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_env_cfg.py @@ -1,10 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg from isaaclab.envs import DirectRLEnvCfg from isaaclab.scene import InteractiveSceneCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_tasks_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_tasks_cfg.py index f21002160101..b651767f7f35 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_tasks_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/assembly_tasks_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_algo_utils.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_algo_utils.py index 86ce3491b161..4588d4e227fd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_algo_utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_algo_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +7,9 @@ import re import subprocess import sys + import torch import trimesh - import warp as wp print("Python Executable:", sys.executable) @@ -72,7 +72,6 @@ def get_cuda_version(): def get_gripper_open_width(obj_filepath): - retrieve_file_path(obj_filepath, download_dir="./") obj_mesh = trimesh.load_mesh(os.path.basename(obj_filepath)) # obj_mesh = trimesh.load_mesh(obj_filepath) @@ -114,7 +113,6 @@ def get_closest_state_idx(ref_traj, curr_ee_pos): def get_reward_mask(ref_traj, curr_ee_pos, tolerance): - _, min_dist_step_idx, _ = get_closest_state_idx(ref_traj, curr_ee_pos) selected_steps = torch.index_select( ref_traj, dim=1, index=min_dist_step_idx diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_log_utils.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_log_utils.py index 9f80d820aa91..f46fcb3479bc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_log_utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/automate_log_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +7,6 @@ def write_log_to_hdf5(held_asset_pose_log, fixed_asset_pose_log, success_log, eval_logging_filename): - with h5py.File(eval_logging_filename, "w") as hf: hf.create_dataset("held_asset_pose", data=held_asset_pose_log.cpu().numpy()) hf.create_dataset("fixed_asset_pose", data=fixed_asset_pose_log.cpu().numpy()) @@ -15,7 +14,6 @@ def write_log_to_hdf5(held_asset_pose_log, fixed_asset_pose_log, success_log, ev def load_log_from_hdf5(eval_logging_filename): - with h5py.File(eval_logging_filename, "r") as hf: held_asset_pose = hf["held_asset_pose"][:] fixed_asset_pose = hf["fixed_asset_pose"][:] diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env.py index c74b5da124b4..a4b454829eac 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env.py @@ -1,11 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import json -import numpy as np import os + +import numpy as np import torch import carb @@ -27,7 +28,6 @@ class DisassemblyEnv(DirectRLEnv): cfg: DisassemblyEnvCfg def __init__(self, cfg: DisassemblyEnvCfg, render_mode: str | None = None, **kwargs): - # Update number of obs/states cfg.observation_space = sum([OBS_DIM_CFG[obs] for obs in cfg.obs_order]) cfg.state_space = sum([STATE_DIM_CFG[state] for state in cfg.state_order]) @@ -426,7 +426,6 @@ def _get_dones(self): time_out = self.episode_length_buf >= self.max_episode_length - 1 if time_out[0]: - self.close_gripper(env_ids=np.array(range(self.num_envs)).reshape(-1)) self._disassemble_plug_from_socket() @@ -551,7 +550,6 @@ def set_pos_inverse_kinematics(self, env_ids): return pos_error, axis_angle_error def _move_gripper_to_eef_pose(self, env_ids, goal_pos, goal_quat, sim_steps, if_log=False): - for _ in range(sim_steps): if if_log: self._log_robot_state_per_timestep() @@ -617,7 +615,6 @@ def step_sim_no_action(self): self._compute_intermediate_values(dt=self.physics_dt) def randomize_fixed_initial_state(self, env_ids): - # (1.) Randomize fixed asset pose. fixed_state = self._fixed_asset.data.default_root_state.clone()[env_ids] # (1.a.) Position @@ -655,7 +652,6 @@ def randomize_fixed_initial_state(self, env_ids): self.step_sim_no_action() def randomize_held_initial_state(self, env_ids, pre_grasp): - # Set plug pos to assembled state held_state = self._held_asset.data.default_root_state.clone() held_state[env_ids, 0:3] = self.fixed_pos[env_ids].clone() + self.scene.env_origins[env_ids] @@ -749,7 +745,6 @@ def _lift_gripper(self, lift_distance, sim_steps, env_ids=None): """Lift gripper by specified distance. Called outside RL loop (i.e., after last step of episode).""" ctrl_tgt_pos = torch.empty_like(self.fingertip_midpoint_pos).copy_(self.fingertip_midpoint_pos) - # ctrl_tgt_quat = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float32, device=self.device).repeat((self.num_envs,1)) ctrl_tgt_quat = torch.empty_like(self.fingertip_midpoint_quat).copy_(self.fingertip_midpoint_quat) ctrl_tgt_pos[:, 2] += lift_distance if len(env_ids) == 0: @@ -798,7 +793,6 @@ def _randomize_gripper_pose(self, sim_steps, env_ids): self._move_gripper_to_eef_pose(env_ids, ctrl_tgt_pos, ctrl_tgt_quat, sim_steps, if_log=True) def _init_log_data_per_assembly(self): - self.log_assembly_id = [] self.log_plug_pos = [] self.log_plug_quat = [] @@ -811,7 +805,6 @@ def _init_log_data_per_assembly(self): self.log_arm_dof_pos = [] def _init_log_data_per_episode(self): - self.log_fingertip_centered_pos_traj = [] self.log_fingertip_centered_quat_traj = [] self.log_arm_dof_pos_traj = [] @@ -824,7 +817,6 @@ def _init_log_data_per_episode(self): self.init_plug_quat = self.held_quat.clone().detach() def _log_robot_state(self, env_ids): - self.log_plug_pos += torch.stack(self.log_plug_pos_traj, dim=1)[env_ids].cpu().tolist() self.log_plug_quat += torch.stack(self.log_plug_quat_traj, dim=1)[env_ids].cpu().tolist() self.log_arm_dof_pos += torch.stack(self.log_arm_dof_pos_traj, dim=1)[env_ids].cpu().tolist() @@ -836,7 +828,6 @@ def _log_robot_state(self, env_ids): ) def _log_robot_state_per_timestep(self): - self.log_plug_pos_traj.append(self.held_pos.clone().detach()) self.log_plug_quat_traj.append(self.held_quat.clone().detach()) self.log_arm_dof_pos_traj.append(self.joint_pos[:, 0:7].clone().detach()) @@ -844,16 +835,13 @@ def _log_robot_state_per_timestep(self): self.log_fingertip_centered_quat_traj.append(self.fingertip_midpoint_quat.clone().detach()) def _log_object_state(self, env_ids): - self.log_plug_grasp_pos += self.init_plug_grasp_pos[env_ids].cpu().tolist() self.log_plug_grasp_quat += self.init_plug_grasp_quat[env_ids].cpu().tolist() self.log_init_plug_pos += self.init_plug_pos[env_ids].cpu().tolist() self.log_init_plug_quat += self.init_plug_quat[env_ids].cpu().tolist() def _save_log_traj(self): - if len(self.log_arm_dof_pos) > self.cfg_task.num_log_traj: - log_item = [] for i in range(self.cfg_task.num_log_traj): curr_dict = dict({}) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env_cfg.py index 6f6630a3eb84..9d17f3597587 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_env_cfg.py @@ -1,10 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg from isaaclab.envs import DirectRLEnvCfg from isaaclab.scene import InteractiveSceneCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_tasks_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_tasks_cfg.py index 9308f281491d..d21bd0166e5c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_tasks_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/disassembly_tasks_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/factory_control.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/factory_control.py index 6bb2c7264027..0e51b6e41f6c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/factory_control.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/factory_control.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +9,7 @@ """ import math + import torch import isaacsim.core.utils.torch as torch_utils @@ -65,8 +66,7 @@ def compute_dof_torque( jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) dof_torque[:, 0:7] = (jacobian_T @ task_wrench.unsqueeze(-1)).squeeze(-1) - # 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 + # adapted from roboticsproceedings.org/rss07/p31.pdf # useful tensors arm_mass_matrix_inv = torch.inverse(arm_mass_matrix) @@ -118,9 +118,7 @@ def get_pose_error( fingertip_midpoint_quat_norm = torch_utils.quat_mul( fingertip_midpoint_quat, torch_utils.quat_conjugate(fingertip_midpoint_quat) - )[ - :, 0 - ] # scalar component + )[:, 0] # scalar component fingertip_midpoint_quat_inv = torch_utils.quat_conjugate( fingertip_midpoint_quat ) / fingertip_midpoint_quat_norm.unsqueeze(-1) @@ -133,13 +131,15 @@ def get_pose_error( return pos_error, quat_error elif rot_error_type == "axis_angle": return pos_error, axis_angle_error + else: + raise ValueError(f"Unsupported rotation error type: {rot_error_type}. Valid: 'quat', 'axis_angle'.") def _get_delta_dof_pos(delta_pose, ik_method, jacobian, device): """Get delta Franka DOF position from delta pose using specified IK method.""" # References: # 1) https://www.cs.cmu.edu/~15464-s13/lectures/lecture6/iksurvey.pdf - # 2) https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2018/RD_HS2018script.pdf (p. 47) + # 2) https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2018/RD_HS2018script.pdf (p. 47) # noqa: E501 if ik_method == "pinv": # Jacobian pseudoinverse k_val = 1.0 @@ -165,7 +165,7 @@ def _get_delta_dof_pos(delta_pose, ik_method, jacobian, device): U, S, Vh = torch.linalg.svd(jacobian) S_inv = 1.0 / S min_singular_value = 1.0e-5 - S_inv = torch.where(S > min_singular_value, S_inv, torch.zeros_like(S_inv)) + S_inv = torch.where(min_singular_value < S, S_inv, torch.zeros_like(S_inv)) jacobian_pinv = ( torch.transpose(Vh, dim0=1, dim1=2)[:, :, :6] @ torch.diag_embed(S_inv) @ torch.transpose(U, dim0=1, dim1=2) ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/industreal_algo_utils.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/industreal_algo_utils.py index 89cf4d2553df..c324eb46f46f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/industreal_algo_utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/industreal_algo_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -33,23 +33,28 @@ """IndustReal: algorithms module. -Contains functions that implement Simulation-Aware Policy Update (SAPU), SDF-Based Reward, and Sampling-Based Curriculum (SBC). +Contains functions that implement: + +- Simulation-Aware Policy Update (SAPU) +- SDF-Based Reward +- Sampling-Based Curriculum (SBC) Not intended to be executed as a standalone script. """ # Force garbage collection for large arrays import gc -import numpy as np import os +import numpy as np + # from pysdf import SDF import torch import trimesh -from trimesh.exchange.load import load # from urdfpy import URDF import warp as wp +from trimesh.exchange.load import load from isaaclab.utils.assets import retrieve_file_path @@ -105,7 +110,6 @@ def get_sdf_reward( sdf_reward = torch.zeros((num_envs,), dtype=torch.float32, device=device) for i in range(num_envs): - # Create copy of plug mesh mesh_points = wp.clone(wp_plug_mesh.points) mesh_indices = wp.clone(wp_plug_mesh.indices) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_disassembly_w_id.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_disassembly_w_id.py index 5eab5d3f6d4a..89c8a39650bd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_disassembly_w_id.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_disassembly_w_id.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_w_id.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_w_id.py index 4d1aab2e813a..18e8914e6704 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_w_id.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/run_w_id.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/automate/soft_dtw_cuda.py b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/soft_dtw_cuda.py index e3e74f0a075a..a979ec449381 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/automate/soft_dtw_cuda.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/automate/soft_dtw_cuda.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -27,12 +27,12 @@ # ---------------------------------------------------------------------------------------------------------------------- import math + import numpy as np import torch import torch.cuda -from torch.autograd import Function - from numba import cuda, jit, prange +from torch.autograd import Function # ---------------------------------------------------------------------------------------------------------------------- @@ -52,7 +52,6 @@ def compute_softdtw_cuda(D, gamma, bandwidth, max_i, max_j, n_passes, R): # Go over each anti-diagonal. Only process threads that fall on the current on the anti-diagonal for p in range(n_passes): - # The index is actually 'p - tid' but need to force it in-bounds J = max(0, min(p - tid, max_j - 1)) @@ -61,7 +60,7 @@ def compute_softdtw_cuda(D, gamma, bandwidth, max_i, max_j, n_passes, R): j = J + 1 # Only compute if element[i, j] is on the current anti-diagonal, and also is within bounds - if tid + J == p and (tid < max_i and J < max_j): + if tid + J == p and (tid < max_i and max_j > J): # Don't compute if outside bandwidth if not (abs(i - j) > bandwidth > 0): r0 = -R[b, i - 1, j - 1] * inv_gamma @@ -96,8 +95,7 @@ def compute_softdtw_backward_cuda(D, R, inv_gamma, bandwidth, max_i, max_j, n_pa j = J + 1 # Only compute if element[i, j] is on the current anti-diagonal, and also is within bounds - if tid + J == rev_p and (tid < max_i and J < max_j): - + if tid + J == rev_p and (tid < max_i and max_j > J): if math.isinf(R[k, i, j]): R[k, i, j] = -math.inf @@ -138,7 +136,8 @@ def forward(ctx, D, device, gamma, bandwidth): # Run the CUDA kernel. # Set CUDA's grid size to be equal to the batch size (every CUDA block processes one sample pair) - # Set the CUDA block size to be equal to the length of the longer sequence (equal to the size of the largest diagonal) + # Set the CUDA block size to be equal to the length of the longer sequence + # (equal to the size of the largest diagonal) compute_softdtw_cuda[B, threads_per_block]( cuda.as_cuda_array(D.detach()), gamma.item(), bandwidth.item(), N, M, n_passes, cuda.as_cuda_array(R) ) @@ -199,7 +198,6 @@ def compute_softdtw(D, gamma, bandwidth): for b in prange(B): for j in range(1, M + 1): for i in range(1, N + 1): - # Check the pruning condition if 0 < bandwidth < np.abs(i - j): continue @@ -230,7 +228,6 @@ def compute_softdtw_backward(D_, R, gamma, bandwidth): for k in prange(B): for j in range(M, 0, -1): for i in range(N, 0, -1): - if np.isinf(R[k, i, j]): R[k, i, j] = -np.inf @@ -287,15 +284,19 @@ class SoftDTW(torch.nn.Module): """ def __init__(self, use_cuda, device, gamma=1.0, normalize=False, bandwidth=None, dist_func=None): - """ - Initializes a new instance using the supplied parameters - :param use_cuda: Flag indicating whether the CUDA implementation should be used - :param device: device to run the soft dtw computation - :param gamma: sDTW's gamma parameter - :param normalize: Flag indicating whether to perform normalization - (as discussed in https://github.com/mblondel/soft-dtw/issues/10#issuecomment-383564790) - :param bandwidth: Sakoe-Chiba bandwidth for pruning. Passing 'None' will disable pruning. - :param dist_func: Optional point-wise distance function to use. If 'None', then a default Euclidean distance function will be used. + """Initializes a new instance using the supplied parameters + + Args: + + use_cuda: Whether to use the CUDA implementation. + device: The device to run the SoftDTW computation. + gamma: The SoftDTW's gamma parameter. Default is 1.0. + normalize: Whether to perform normalization. Default is False. + (as discussed in https://github.com/mblondel/soft-dtw/issues/10#issuecomment-383564790) + bandwidth: Sakoe-Chiba bandwidth for pruning. Default is None, which disables pruning. + If provided, must be a float. + dist_func: The point-wise distance function to use. Default is None, which + uses a default Euclidean distance function. """ super().__init__() self.normalize = normalize @@ -403,9 +404,8 @@ def profile(batch_size, seq_len_a, seq_len_b, dims, tol_backward): n_iters = 6 print( - "Profiling forward() + backward() times for batch_size={}, seq_len_a={}, seq_len_b={}, dims={}...".format( - batch_size, seq_len_a, seq_len_b, dims - ) + f"Profiling forward() + backward() times for batch_size={batch_size}, seq_len_a={seq_len_a}," + f" seq_len_b={seq_len_b}, dims={dims}..." ) times_cpu = [] @@ -427,9 +427,9 @@ def profile(batch_size, seq_len_a, seq_len_b, dims, tol_backward): assert torch.allclose(forward_cpu, forward_gpu.cpu()) assert torch.allclose(backward_cpu, backward_gpu.cpu(), atol=tol_backward) - if ( - i > 0 - ): # Ignore the first time we run, in case this is a cold start (because timings are off at a cold start of the script) + # Ignore the first time we run, in case this is a cold start + # (because timings are off at a cold start of the script) + if i > 0: times_cpu += [t_cpu] times_gpu += [t_gpu] @@ -444,7 +444,6 @@ def profile(batch_size, seq_len_a, seq_len_b, dims, tol_backward): # ---------------------------------------------------------------------------------------------------------------------- if __name__ == "__main__": - torch.manual_seed(1234) profile(128, 17, 15, 2, tol_backward=1e-6) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/__init__.py index 11f3b2b631b3..b4913fb2c3a9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/rl_games_ppo_cfg.yaml index a54d6d3e2bc4..2e97a86a62ff 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ippo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ippo_cfg.yaml index 2f66ad8d20ad..f9298c9252ac 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ippo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ippo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_mappo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_mappo_cfg.yaml index ee30acb3484a..8f192cf2988d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_mappo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_mappo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ppo_cfg.yaml index c053b5b00353..c5f7943b99d9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py index 8713e9220576..e0464a7201c8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cart_double_pendulum/cart_double_pendulum_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,10 +6,9 @@ from __future__ import annotations import math -import torch from collections.abc import Sequence -from isaaclab_assets.robots.cart_double_pendulum import CART_DOUBLE_PENDULUM_CFG +import torch import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, ArticulationCfg @@ -20,6 +19,8 @@ from isaaclab.utils import configclass from isaaclab.utils.math import sample_uniform +from isaaclab_assets.robots.cart_double_pendulum import CART_DOUBLE_PENDULUM_CFG + @configclass class CartDoublePendulumEnvCfg(DirectMARLEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/__init__.py index 7b2b689c7de3..f72ee0a6f8dc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_camera_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_camera_ppo_cfg.yaml index c9d5c9cce35f..60c37b40476d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_camera_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_camera_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_ppo_cfg.yaml index 5cc18cb90f7f..a673a29257ea 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rsl_rl_ppo_cfg.py index 1cadf22d48c0..097b7b43a672 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/sb3_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/sb3_ppo_cfg.yaml index 698101cea0ef..fcb32cd51dd9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/sb3_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/sb3_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_camera_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_camera_ppo_cfg.yaml index 17fcf9c72715..282ebb0020c5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_camera_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_camera_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_ppo_cfg.yaml index 83bcf50162a9..b50b4bea69bb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_camera_env.py index 91cd563ed7f5..9606008ccf19 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_camera_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,10 +6,9 @@ from __future__ import annotations import math -import torch from collections.abc import Sequence -from isaaclab_assets.robots.cartpole import CARTPOLE_CFG +import torch import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, ArticulationCfg @@ -20,6 +19,8 @@ from isaaclab.utils import configclass from isaaclab.utils.math import sample_uniform +from isaaclab_assets.robots.cartpole import CARTPOLE_CFG + @configclass class CartpoleRGBCameraEnvCfg(DirectRLEnvCfg): @@ -91,6 +92,7 @@ class CartpoleDepthCameraEnvCfg(CartpoleRGBCameraEnvCfg): class CartpoleCameraEnv(DirectRLEnv): + """Cartpole Camera Environment.""" cfg: CartpoleRGBCameraEnvCfg | CartpoleDepthCameraEnvCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_env.py index d9e84581ed91..f897b64f3ec9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole/cartpole_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,10 +6,9 @@ from __future__ import annotations import math -import torch from collections.abc import Sequence -from isaaclab_assets.robots.cartpole import CARTPOLE_CFG +import torch import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, ArticulationCfg @@ -20,6 +19,8 @@ from isaaclab.utils import configclass from isaaclab.utils.math import sample_uniform +from isaaclab_assets.robots.cartpole import CARTPOLE_CFG + @configclass class CartpoleEnvCfg(DirectRLEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/__init__.py index 64d6dbb11e8f..d401c413967d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/__init__.py index a6f51f93c007..576ccc822edf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_box_ppo_cfg.yaml index 4f9207baddf0..08d0e729708a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_discrete_ppo_cfg.yaml index 6b99110b6107..4d7852d6665c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_multidiscrete_ppo_cfg.yaml index 4a688eb04d42..47a764f6117d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_box_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_box_ppo_cfg.yaml index 8ddcb33bb4ff..7153e46bcf13 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_discrete_ppo_cfg.yaml index 5b51dd99137c..67bb6b932dcf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_multidiscrete_ppo_cfg.yaml index 7259f38d6436..d51d71764315 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_dict_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_box_ppo_cfg.yaml index e6bd264cd005..f55aa1f21c67 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_discrete_ppo_cfg.yaml index fe13ad35d292..ae513a501835 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_multidiscrete_ppo_cfg.yaml index 212fae2e4700..7310ed646ca4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_discrete_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_box_ppo_cfg.yaml index 0867fdabb419..1ed10841aa02 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_discrete_ppo_cfg.yaml index 0d5e06a99b2f..5ed49c9f6692 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_multidiscrete_ppo_cfg.yaml index 599901ae3cac..ad95dab1ba63 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_multidiscrete_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_box_ppo_cfg.yaml index 87e09b7206f3..291610cc73d3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_discrete_ppo_cfg.yaml index 8471617d2cd0..cd48d89491ed 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml index b14671ab340e..84ba7d6506be 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env.py index 3d96318443a2..dc03eb299d0d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -42,7 +42,6 @@ def _apply_action(self) -> None: self.cartpole.set_joint_effort_target(target, joint_ids=self._cart_dof_idx) def _get_observations(self) -> dict: - # fundamental spaces # - Box if isinstance(self.single_observation_space["policy"], gym.spaces.Box): diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env_cfg.py index bed8ce0abf38..e6e8169b1ba0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole/cartpole_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -421,10 +421,12 @@ class DictBoxEnvCfg(CartpoleEnvCfg): === === """ - observation_space = spaces.Dict({ - "joint-positions": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - }) # or for simplicity: {"joint-positions": 2, "joint-velocities": 2} + observation_space = spaces.Dict( + { + "joint-positions": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + } + ) # or for simplicity: {"joint-positions": 2, "joint-velocities": 2} action_space = spaces.Box(low=-1.0, high=1.0, shape=(1,)) # or for simplicity: 1 or [1] @@ -451,10 +453,12 @@ class DictDiscreteEnvCfg(CartpoleEnvCfg): === === """ - observation_space = spaces.Dict({ - "joint-positions": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - }) # or for simplicity: {"joint-positions": 2, "joint-velocities": 2} + observation_space = spaces.Dict( + { + "joint-positions": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + } + ) # or for simplicity: {"joint-positions": 2, "joint-velocities": 2} action_space = spaces.Discrete(3) # or for simplicity: {3} @@ -488,10 +492,12 @@ class DictMultiDiscreteEnvCfg(CartpoleEnvCfg): === === """ - observation_space = spaces.Dict({ - "joint-positions": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - }) # or for simplicity: {"joint-positions": 2, "joint-velocities": 2} + observation_space = spaces.Dict( + { + "joint-positions": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + } + ) # or for simplicity: {"joint-positions": 2, "joint-velocities": 2} action_space = spaces.MultiDiscrete([3, 2]) # or for simplicity: [{3}, {2}] @@ -521,10 +527,12 @@ class TupleBoxEnvCfg(CartpoleEnvCfg): === === """ - observation_space = spaces.Tuple(( - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - )) # or for simplicity: (2, 2) + observation_space = spaces.Tuple( + ( + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + ) + ) # or for simplicity: (2, 2) action_space = spaces.Box(low=-1.0, high=1.0, shape=(1,)) # or for simplicity: 1 or [1] @@ -551,10 +559,12 @@ class TupleDiscreteEnvCfg(CartpoleEnvCfg): === === """ - observation_space = spaces.Tuple(( - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - )) # or for simplicity: (2, 2) + observation_space = spaces.Tuple( + ( + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + ) + ) # or for simplicity: (2, 2) action_space = spaces.Discrete(3) # or for simplicity: {3} @@ -588,8 +598,10 @@ class TupleMultiDiscreteEnvCfg(CartpoleEnvCfg): === === """ - observation_space = spaces.Tuple(( - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - )) # or for simplicity: (2, 2) + observation_space = spaces.Tuple( + ( + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + ) + ) # or for simplicity: (2, 2) action_space = spaces.MultiDiscrete([3, 2]) # or for simplicity: [{3}, {2}] diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/__init__.py index e111e4b421da..2953ce1a29c0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_box_ppo_cfg.yaml index eadc5c76204f..77bb2c8eab50 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_discrete_ppo_cfg.yaml index c565b37e76ee..ac047e6b9361 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_multidiscrete_ppo_cfg.yaml index ffaa26ef7a4e..8710230931d0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_box_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_box_ppo_cfg.yaml index f08bd3da1bd0..09f598574135 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_discrete_ppo_cfg.yaml index 757ba53d1305..92a5c1f52b4e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_multidiscrete_ppo_cfg.yaml index 78df49b19c73..2dfd9f889eaf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_dict_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_box_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_box_ppo_cfg.yaml index 7c8dd68e58e0..423f17203ccc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_box_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_box_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_discrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_discrete_ppo_cfg.yaml index c3c720276715..f5aafefa9060 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_discrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_discrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml index 34d706dd9d20..5e3637aeb7b2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/agents/skrl_tuple_multidiscrete_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env.py index 1c2563294ac2..1c8dc6b4a078 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py index 74af75d9ea89..5e146041b79b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/cartpole_showcase/cartpole_camera/cartpole_camera_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -163,10 +163,14 @@ class DictBoxEnvCfg(CartpoleCameraEnvCfg): tiled_camera: TiledCameraCfg = get_tiled_camera_cfg("rgb") # spaces - observation_space = spaces.Dict({ - "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - "camera": spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), - }) # or for simplicity: {"joint-velocities": 2, "camera": [height, width, 3]} + observation_space = spaces.Dict( + { + "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + "camera": spaces.Box( + low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3) + ), + } + ) # or for simplicity: {"joint-velocities": 2, "camera": [height, width, 3]} action_space = spaces.Box(low=-1.0, high=1.0, shape=(1,)) # or for simplicity: 1 or [1] @@ -197,10 +201,14 @@ class DictDiscreteEnvCfg(CartpoleCameraEnvCfg): tiled_camera: TiledCameraCfg = get_tiled_camera_cfg("rgb") # spaces - observation_space = spaces.Dict({ - "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - "camera": spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), - }) # or for simplicity: {"joint-velocities": 2, "camera": [height, width, 3]} + observation_space = spaces.Dict( + { + "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + "camera": spaces.Box( + low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3) + ), + } + ) # or for simplicity: {"joint-velocities": 2, "camera": [height, width, 3]} action_space = spaces.Discrete(3) # or for simplicity: {3} @@ -238,10 +246,14 @@ class DictMultiDiscreteEnvCfg(CartpoleCameraEnvCfg): tiled_camera: TiledCameraCfg = get_tiled_camera_cfg("rgb") # spaces - observation_space = spaces.Dict({ - "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - "camera": spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), - }) # or for simplicity: {"joint-velocities": 2, "camera": [height, width, 3]} + observation_space = spaces.Dict( + { + "joint-velocities": spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + "camera": spaces.Box( + low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3) + ), + } + ) # or for simplicity: {"joint-velocities": 2, "camera": [height, width, 3]} action_space = spaces.MultiDiscrete([3, 2]) # or for simplicity: [{3}, {2}] @@ -275,10 +287,12 @@ class TupleBoxEnvCfg(CartpoleCameraEnvCfg): tiled_camera: TiledCameraCfg = get_tiled_camera_cfg("rgb") # spaces - observation_space = spaces.Tuple(( - spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - )) # or for simplicity: ([height, width, 3], 2) + observation_space = spaces.Tuple( + ( + spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + ) + ) # or for simplicity: ([height, width, 3], 2) action_space = spaces.Box(low=-1.0, high=1.0, shape=(1,)) # or for simplicity: 1 or [1] @@ -309,10 +323,12 @@ class TupleDiscreteEnvCfg(CartpoleCameraEnvCfg): tiled_camera: TiledCameraCfg = get_tiled_camera_cfg("rgb") # spaces - observation_space = spaces.Tuple(( - spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - )) # or for simplicity: ([height, width, 3], 2) + observation_space = spaces.Tuple( + ( + spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + ) + ) # or for simplicity: ([height, width, 3], 2) action_space = spaces.Discrete(3) # or for simplicity: {3} @@ -350,8 +366,10 @@ class TupleMultiDiscreteEnvCfg(CartpoleCameraEnvCfg): tiled_camera: TiledCameraCfg = get_tiled_camera_cfg("rgb") # spaces - observation_space = spaces.Tuple(( - spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), - spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), - )) # or for simplicity: ([height, width, 3], 2) + observation_space = spaces.Tuple( + ( + spaces.Box(low=float("-inf"), high=float("inf"), shape=(tiled_camera.height, tiled_camera.width, 3)), + spaces.Box(low=float("-inf"), high=float("inf"), shape=(2,)), + ) + ) # or for simplicity: ([height, width, 3], 2) action_space = spaces.MultiDiscrete([3, 2]) # or for simplicity: [{3}, {2}] diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/__init__.py index 70fa6f3c7d58..54d69f31f679 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,8 +6,6 @@ import gymnasium as gym from . import agents -from .factory_env import FactoryEnv -from .factory_env_cfg import FactoryTaskGearMeshCfg, FactoryTaskNutThreadCfg, FactoryTaskPegInsertCfg ## # Register Gym environments. @@ -15,30 +13,30 @@ gym.register( id="Isaac-Factory-PegInsert-Direct-v0", - entry_point="isaaclab_tasks.direct.factory:FactoryEnv", + entry_point=f"{__name__}.factory_env:FactoryEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": FactoryTaskPegInsertCfg, + "env_cfg_entry_point": f"{__name__}.factory_env_cfg:FactoryTaskPegInsertCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", }, ) gym.register( id="Isaac-Factory-GearMesh-Direct-v0", - entry_point="isaaclab_tasks.direct.factory:FactoryEnv", + entry_point=f"{__name__}.factory_env:FactoryEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": FactoryTaskGearMeshCfg, + "env_cfg_entry_point": f"{__name__}.factory_env_cfg:FactoryTaskGearMeshCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", }, ) gym.register( id="Isaac-Factory-NutThread-Direct-v0", - entry_point="isaaclab_tasks.direct.factory:FactoryEnv", + entry_point=f"{__name__}.factory_env:FactoryEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": FactoryTaskNutThreadCfg, + "env_cfg_entry_point": f"{__name__}.factory_env_cfg:FactoryTaskNutThreadCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml index 35777c2dcf9d..f11f6d996745 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_control.py b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_control.py index 7cbf4c957f81..f8ccb0e13451 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_control.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_control.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +9,7 @@ """ import math + import torch import isaacsim.core.utils.torch as torch_utils @@ -75,8 +76,7 @@ def compute_dof_torque( jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) dof_torque[:, 0:7] = (jacobian_T @ task_wrench.unsqueeze(-1)).squeeze(-1) - # 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 + # adapted from roboticsproceedings.org/rss07/p31.pdf # useful tensors arm_mass_matrix_inv = torch.inverse(arm_mass_matrix) @@ -128,9 +128,7 @@ def get_pose_error( fingertip_midpoint_quat_norm = torch_utils.quat_mul( fingertip_midpoint_quat, torch_utils.quat_conjugate(fingertip_midpoint_quat) - )[ - :, 0 - ] # scalar component + )[:, 0] # scalar component fingertip_midpoint_quat_inv = torch_utils.quat_conjugate( fingertip_midpoint_quat ) / fingertip_midpoint_quat_norm.unsqueeze(-1) @@ -143,13 +141,15 @@ def get_pose_error( return pos_error, quat_error elif rot_error_type == "axis_angle": return pos_error, axis_angle_error + else: + raise ValueError(f"Unsupported rotation error type: {rot_error_type}. Valid: 'quat', 'axis_angle'.") def get_delta_dof_pos(delta_pose, ik_method, jacobian, device): """Get delta Franka DOF position from delta pose using specified IK method.""" # References: # 1) https://www.cs.cmu.edu/~15464-s13/lectures/lecture6/iksurvey.pdf - # 2) https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2018/RD_HS2018script.pdf (p. 47) + # 2) https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2018/RD_HS2018script.pdf (p. 47) # noqa: E501 if ik_method == "pinv": # Jacobian pseudoinverse k_val = 1.0 @@ -175,7 +175,7 @@ def get_delta_dof_pos(delta_pose, ik_method, jacobian, device): U, S, Vh = torch.linalg.svd(jacobian) S_inv = 1.0 / S min_singular_value = 1.0e-5 - S_inv = torch.where(S > min_singular_value, S_inv, torch.zeros_like(S_inv)) + S_inv = torch.where(min_singular_value < S, S_inv, torch.zeros_like(S_inv)) jacobian_pinv = ( torch.transpose(Vh, dim0=1, dim1=2)[:, :, :6] @ torch.diag_embed(S_inv) @ torch.transpose(U, dim0=1, dim1=2) ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env.py index c4dfed30e026..a4e9c6d9ece9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env_cfg.py index 735efa306d60..0e6b5db0a73c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_env_cfg.py @@ -1,10 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg from isaaclab.envs import DirectRLEnvCfg from isaaclab.scene import InteractiveSceneCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_tasks_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_tasks_cfg.py index 9b415458ec4e..c631856816cb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_tasks_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_tasks_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_utils.py b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_utils.py index c831323ee6b7..962b3872bf09 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/factory/factory_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/forge/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/__init__.py index 72af6221dc44..6532e3c3b6b5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,8 +6,6 @@ import gymnasium as gym from . import agents -from .forge_env import ForgeEnv -from .forge_env_cfg import ForgeTaskGearMeshCfg, ForgeTaskNutThreadCfg, ForgeTaskPegInsertCfg ## # Register Gym environments. @@ -15,30 +13,30 @@ gym.register( id="Isaac-Forge-PegInsert-Direct-v0", - entry_point="isaaclab_tasks.direct.forge:ForgeEnv", + entry_point=f"{__name__}.forge_env:ForgeEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ForgeTaskPegInsertCfg, + "env_cfg_entry_point": f"{__name__}.forge_env_cfg:ForgeTaskPegInsertCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", }, ) gym.register( id="Isaac-Forge-GearMesh-Direct-v0", - entry_point="isaaclab_tasks.direct.forge:ForgeEnv", + entry_point=f"{__name__}.forge_env:ForgeEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ForgeTaskGearMeshCfg, + "env_cfg_entry_point": f"{__name__}.forge_env_cfg:ForgeTaskGearMeshCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", }, ) gym.register( id="Isaac-Forge-NutThread-Direct-v0", - entry_point="isaaclab_tasks.direct.forge:ForgeEnv", + entry_point=f"{__name__}.forge_env:ForgeEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": ForgeTaskNutThreadCfg, + "env_cfg_entry_point": f"{__name__}.forge_env_cfg:ForgeTaskNutThreadCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg_nut_thread.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg.yaml index 4b42c6edc6f7..08081f97e035 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg_nut_thread.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg_nut_thread.yaml index 76e2641a3c45..a73dd178f6e0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg_nut_thread.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/agents/rl_games_ppo_cfg_nut_thread.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env.py index c4c842100194..75484cbd8f17 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -121,21 +121,25 @@ def _get_observations(self): prev_actions = self.actions.clone() prev_actions[:, 3:5] = 0.0 - obs_dict.update({ - "fingertip_pos": self.noisy_fingertip_pos, - "fingertip_pos_rel_fixed": self.noisy_fingertip_pos - noisy_fixed_pos, - "fingertip_quat": self.noisy_fingertip_quat, - "force_threshold": self.contact_penalty_thresholds[:, None], - "ft_force": self.noisy_force, - "prev_actions": prev_actions, - }) - - state_dict.update({ - "ema_factor": self.ema_factor, - "ft_force": self.force_sensor_smooth[:, 0:3], - "force_threshold": self.contact_penalty_thresholds[:, None], - "prev_actions": prev_actions, - }) + obs_dict.update( + { + "fingertip_pos": self.noisy_fingertip_pos, + "fingertip_pos_rel_fixed": self.noisy_fingertip_pos - noisy_fixed_pos, + "fingertip_quat": self.noisy_fingertip_quat, + "force_threshold": self.contact_penalty_thresholds[:, None], + "ft_force": self.noisy_force, + "prev_actions": prev_actions, + } + ) + + state_dict.update( + { + "ema_factor": self.ema_factor, + "ft_force": self.force_sensor_smooth[:, 0:3], + "force_threshold": self.contact_penalty_thresholds[:, None], + "prev_actions": prev_actions, + } + ) obs_tensors = factory_utils.collapse_obs_dict(obs_dict, self.cfg.obs_order + ["prev_actions"]) state_tensors = factory_utils.collapse_obs_dict(state_dict, self.cfg.state_order + ["prev_actions"]) @@ -190,7 +194,8 @@ def _apply_action(self): desired_xyz = torch.stack([desired_roll, desired_pitch, desired_yaw], dim=1) # (2.b.ii) Correct the direction of motion to avoid joint limit. - # Map yaws between [-125, 235] degrees (so that angles appear on a continuous span uninterrupted by the joint limit). + # Map yaws between [-125, 235] degrees + # (so that angles appear on a continuous span uninterrupted by the joint limit) curr_yaw = factory_utils.wrap_yaw(curr_yaw) desired_yaw = factory_utils.wrap_yaw(desired_yaw) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env_cfg.py index 44ac6531243b..5da73aa2ae35 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -85,7 +85,9 @@ class EventCfg: ) dead_zone_thresholds = EventTerm( - func=randomize_dead_zone, mode="interval", interval_range_s=(2.0, 2.0) # (0.25, 0.25) + func=randomize_dead_zone, + mode="interval", + interval_range_s=(2.0, 2.0), # (0.25, 0.25) ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_events.py b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_events.py index c9eee53cc27a..15ced1c2b1a9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_events.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_tasks_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_tasks_cfg.py index cba3175fa2d7..1529543e1889 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_tasks_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_tasks_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_utils.py b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_utils.py index 177136b101cf..e966cf93f218 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/forge/forge_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/__init__.py index 8b56cc3a689f..c282be85730f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rl_games_ppo_cfg.yaml index 31dfbd9eb843..d4d882905a6c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rsl_rl_ppo_cfg.py index 74788e7b220c..a2304fb2c4b7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/skrl_ppo_cfg.yaml index d1cf5a6b5df6..e1c7fe9676e2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/franka_cabinet_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/franka_cabinet_env.py index 8e0aab5b0c73..8b87e1bdb258 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/franka_cabinet_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/franka_cabinet/franka_cabinet_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,16 +7,16 @@ import torch -from isaacsim.core.utils.stage import get_current_stage from isaacsim.core.utils.torch.transformations import tf_combine, tf_inverse, tf_vector from pxr import UsdGeom import isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import Articulation, ArticulationCfg from isaaclab.envs import DirectRLEnv, DirectRLEnvCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sim import SimulationCfg +from isaaclab.sim.utils.stage import get_current_stage from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/__init__.py index ff38052a5cdb..f7e0f518b666 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rl_games_ppo_cfg.yaml index c2ca68586b50..4ff1aced918c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rsl_rl_ppo_cfg.py index 029629225092..778d73f09119 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/skrl_ppo_cfg.yaml index 130d1999ec37..f56d1fc45336 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/humanoid_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/humanoid_env.py index 6c0ad9190807..402409e9d35b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/humanoid_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid/humanoid_env.py @@ -1,12 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -from isaaclab_assets import HUMANOID_CFG - import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg from isaaclab.envs import DirectRLEnvCfg @@ -17,6 +15,8 @@ from isaaclab_tasks.direct.locomotion.locomotion_env import LocomotionEnv +from isaaclab_assets import HUMANOID_CFG + @configclass class HumanoidEnvCfg(DirectRLEnvCfg): diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/__init__.py index c47146474409..36c93d5a1c53 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_dance_amp_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_dance_amp_cfg.yaml index 090d5eb90a69..3071d039b88c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_dance_amp_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_dance_amp_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_run_amp_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_run_amp_cfg.yaml index f74cecfeb64f..0f6fcdc1a03f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_run_amp_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_run_amp_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_walk_amp_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_walk_amp_cfg.yaml index 727258be3ca6..efb34f0d2f5b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_walk_amp_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/agents/skrl_walk_amp_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env.py index 71b1e148ccaa..5d6a01e9d4e4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env_cfg.py index 151a0101782d..c7178f746c3f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/humanoid_amp_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,8 +8,6 @@ import os from dataclasses import MISSING -from isaaclab_assets import HUMANOID_28_CFG - from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg from isaaclab.envs import DirectRLEnvCfg @@ -17,6 +15,8 @@ from isaaclab.sim import PhysxCfg, SimulationCfg from isaaclab.utils import configclass +from isaaclab_assets import HUMANOID_28_CFG + MOTIONS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "motions") diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/__init__.py index af42f2d9b5d6..06a047fc65ec 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_loader.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_loader.py index 27a473759e22..354332de1b24 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_loader.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_loader.py @@ -1,12 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 numpy as np +from __future__ import annotations + import os + +import numpy as np import torch -from typing import Optional class MotionLoader: @@ -71,10 +73,10 @@ def _interpolate( self, a: torch.Tensor, *, - b: Optional[torch.Tensor] = None, - blend: Optional[torch.Tensor] = None, - start: Optional[np.ndarray] = None, - end: Optional[np.ndarray] = None, + b: torch.Tensor | None = None, + blend: torch.Tensor | None = None, + start: np.ndarray | None = None, + end: np.ndarray | None = None, ) -> torch.Tensor: """Linear interpolation between consecutive values. @@ -102,10 +104,10 @@ def _slerp( self, q0: torch.Tensor, *, - q1: Optional[torch.Tensor] = None, - blend: Optional[torch.Tensor] = None, - start: Optional[np.ndarray] = None, - end: Optional[np.ndarray] = None, + q1: torch.Tensor | None = None, + blend: torch.Tensor | None = None, + start: np.ndarray | None = None, + end: np.ndarray | None = None, ) -> torch.Tensor: """Interpolation between consecutive rotations (Spherical Linear Interpolation). @@ -190,13 +192,13 @@ def sample_times(self, num_samples: int, duration: float | None = None) -> np.nd Time samples, between 0 and the specified/motion duration. """ duration = self.duration if duration is None else duration - assert ( - duration <= self.duration - ), f"The specified duration ({duration}) is longer than the motion duration ({self.duration})" + assert duration <= self.duration, ( + f"The specified duration ({duration}) is longer than the motion duration ({self.duration})" + ) return duration * np.random.uniform(low=0.0, high=1.0, size=num_samples) def sample( - self, num_samples: int, times: Optional[np.ndarray] = None, duration: float | None = None + self, num_samples: int, times: np.ndarray | None = None, duration: float | None = None ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Sample motion data. @@ -209,9 +211,13 @@ def sample( If ``times`` is defined, this parameter is ignored. Returns: - Sampled motion DOF positions (with shape (N, num_dofs)), DOF velocities (with shape (N, num_dofs)), - body positions (with shape (N, num_bodies, 3)), body rotations (with shape (N, num_bodies, 4), as wxyz quaternion), - body linear velocities (with shape (N, num_bodies, 3)) and body angular velocities (with shape (N, num_bodies, 3)). + A tuple containing sampled motion data: + - DOF positions (with shape (N, num_dofs)) + - DOF velocities (with shape (N, num_dofs)) + - Body positions (with shape (N, num_bodies, 3)) + - Body rotations (with shape (N, num_bodies, 4), as wxyz quaternion) + - Body linear velocities (with shape (N, num_bodies, 3)) + - Body angular velocities (with shape (N, num_bodies, 3)) """ times = self.sample_times(num_samples, duration) if times is None else times index_0, index_1, blend = self._compute_frame_blend(times) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_viewer.py b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_viewer.py index 4fe3b10ea467..62438f5e3c68 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_viewer.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/humanoid_amp/motions/motion_viewer.py @@ -1,17 +1,17 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause + from __future__ import annotations import matplotlib import matplotlib.animation import matplotlib.pyplot as plt +import mpl_toolkits.mplot3d # noqa: F401 import numpy as np import torch -import mpl_toolkits.mplot3d # noqa: F401 - try: from .motion_loader import MotionLoader except ImportError: diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py index 56caabb01abc..c8d4fbf9e2d0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/inhand_manipulation/inhand_manipulation_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,11 +6,12 @@ from __future__ import annotations -import numpy as np -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import numpy as np +import torch + import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, RigidObject from isaaclab.envs import DirectRLEnv @@ -396,7 +397,6 @@ def compute_rewards( fall_penalty: float, av_factor: float, ): - goal_dist = torch.norm(object_pos - target_pos, p=2, dim=-1) rot_dist = rotation_distance(object_rot, target_rot) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/locomotion_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/locomotion_env.py index a049354d3b41..faac10e1a718 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/locomotion_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/locomotion/locomotion_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/__init__.py index 050aa2b0211c..a1a5c9ef913a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rl_games_ppo_cfg.yaml index b00c5df35bc8..36e2d8f61fbf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rsl_rl_ppo_cfg.py index 86b2c5508382..607d9f0fb0ea 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/skrl_ppo_cfg.yaml index 3353c5786af2..c84e3f5674bd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/quadcopter_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/quadcopter_env.py index a80ce681bdec..02857c63d348 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/quadcopter_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/quadcopter/quadcopter_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -153,7 +153,9 @@ def _pre_physics_step(self, actions: torch.Tensor): self._moment[:, 0, :] = self.cfg.moment_scale * self._actions[:, 1:] def _apply_action(self): - self._robot.set_external_force_and_torque(self._thrust, self._moment, body_ids=self._body_id) + self._robot.permanent_wrench_composer.set_forces_and_torques( + body_ids=self._body_id, forces=self._thrust, torques=self._moment + ) def _get_observations(self) -> dict: desired_pos_b, _ = subtract_frame_transforms( diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/__init__.py index ed316e6e2673..4d2f0f3eee50 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -51,7 +51,9 @@ }, ) -### Vision +# ------- +# Vision +# ------- gym.register( id="Isaac-Repose-Cube-Shadow-Vision-Direct-v0", diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_cfg.yaml index ecdcacd50443..30b3b0b012f4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_ff_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_ff_cfg.yaml index 2fa2912163d5..6724046a9a59 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_ff_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_ff_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_lstm_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_lstm_cfg.yaml index 3a57e6598070..0aea26e9cde6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_lstm_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_lstm_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_vision_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_vision_cfg.yaml index 73ac12661239..43fa1d20fb88 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_vision_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rl_games_ppo_vision_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rsl_rl_ppo_cfg.py index 665c997e635d..6ab4c9e56f5a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ff_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ff_ppo_cfg.yaml index 7ef224f78ebf..0831dd7b4125 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ff_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ff_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ppo_cfg.yaml index cae9a8445e34..9bba87a54552 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py index 82d76ec7f1e1..75a7b6d04a20 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py @@ -1,10 +1,11 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import glob import os + import torch import torch.nn as nn import torchvision @@ -39,9 +40,11 @@ def __init__(self): nn.Linear(128, 27), ) - self.data_transforms = torchvision.transforms.Compose([ - torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ]) + self.data_transforms = torchvision.transforms.Compose( + [ + torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] + ) def forward(self, x): x = x.permute(0, 3, 1, 2) @@ -79,7 +82,8 @@ def __init__(self, cfg: FeatureExtractorCfg, device: str, log_dir: str | None = Args: cfg: Configuration for the feature extractor model. device: Device to run the model on. - log_dir: Directory to save checkpoints. If None, uses local "logs" folder resolved with respect to this file. + log_dir: Directory to save checkpoints. Default is None, which uses the local + "logs" folder resolved relative to this file. """ self.cfg = cfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py index feb2fb29887f..f9c92f18fbe1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_env_cfg.py @@ -1,11 +1,9 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_CFG - import isaaclab.envs.mdp as mdp import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, RigidObjectCfg @@ -20,6 +18,8 @@ from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR from isaaclab.utils.noise import GaussianNoiseCfg, NoiseModelWithAdditiveBiasCfg +from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_CFG + @configclass class EventCfg: @@ -191,6 +191,7 @@ class ShadowHandEnvCfg(DirectRLEnvCfg): max_depenetration_velocity=1000.0, ), mass_props=sim_utils.MassPropertiesCfg(density=567.0), + semantic_tags=[("class", "cube")], ), init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.39, 0.6), rot=(1.0, 0.0, 0.0, 0.0)), ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py index 42e8c4f03c4b..b5c781c1a9f2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,14 +8,6 @@ import torch -# from Isaac Sim 4.2 onwards, pxr.Semantics is deprecated -try: - import Semantics -except ModuleNotFoundError: - from pxr import Semantics - -from isaacsim.core.utils.stage import get_current_stage - import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, RigidObject from isaaclab.scene import InteractiveSceneCfg @@ -78,15 +70,6 @@ def _setup_scene(self): self.hand = Articulation(self.cfg.robot_cfg) self.object = RigidObject(self.cfg.object_cfg) self._tiled_camera = TiledCamera(self.cfg.tiled_camera) - # get stage - stage = get_current_stage() - # add semantics for in-hand cube - prim = stage.GetPrimAtPath("/World/envs/env_0/object") - sem = Semantics.SemanticsAPI.Apply(prim, "Semantics") - sem.CreateSemanticTypeAttr() - sem.CreateSemanticDataAttr() - sem.GetSemanticTypeAttr().Set("class") - sem.GetSemanticDataAttr().Set("cube") # clone and replicate (no need to filter for this environment) self.scene.clone_environments(copy_from_source=False) # add articulation to scene - we must register to scene to randomize with EventManager diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/__init__.py index 3ac36ac09361..0fbb815b98f2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/rl_games_ppo_cfg.yaml index 19b636debfab..3849c1dd8dab 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ippo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ippo_cfg.yaml index 84f23d446f6e..60be7d18110e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ippo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ippo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_mappo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_mappo_cfg.yaml index 479219a86288..57c1c455185d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_mappo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_mappo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ppo_cfg.yaml index 789738bdf907..9ab45d268337 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env.py index b36a879c1d71..09bbff6e97c0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,9 +6,10 @@ from __future__ import annotations +from collections.abc import Sequence + import numpy as np import torch -from collections.abc import Sequence import isaaclab.sim as sim_utils from isaaclab.assets import Articulation, RigidObject diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py index 8f7e2053b127..855939392a22 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand_over/shadow_hand_over_env_cfg.py @@ -1,11 +1,9 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_CFG - import isaaclab.envs.mdp as mdp import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, RigidObjectCfg @@ -18,6 +16,8 @@ from isaaclab.sim.spawners.materials.physics_materials_cfg import RigidBodyMaterialCfg from isaaclab.utils import configclass +from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_CFG + @configclass class EventCfg: diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/__init__.py index 3ca91bc50066..47e4a4aac392 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/__init__.py index 42b466047ec5..79c13e2aa8f4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/__init__.py index bca53b55404d..df1a4db3a042 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rl_games_ppo_cfg.yaml index 5e2dec84a909..aad56e76525c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rsl_rl_ppo_cfg.py index 5257b0508681..986461733663 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/sb3_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/sb3_ppo_cfg.yaml index 003ec762be5d..126885cbcd44 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/sb3_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/sb3_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/skrl_ppo_cfg.yaml index 4375afee0cb5..3ff54f7e76e8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/ant_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/ant_env_cfg.py index ed1f39a4849c..289d4f75f8c4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/ant_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/ant/ant_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/__init__.py index e5038c6e8aef..68eef31a0cc8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_camera_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_camera_ppo_cfg.yaml index 68845f0882cf..abdccfc4b4ac 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_camera_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_camera_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml index b1a3961b7220..f28e2c85d54a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_ppo_cfg.yaml index 0d26741f4b13..eae63873c89d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rsl_rl_ppo_cfg.py index 86ab5309c362..2a266a098df2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/sb3_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/sb3_ppo_cfg.yaml index 698101cea0ef..fcb32cd51dd9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/sb3_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/sb3_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/skrl_ppo_cfg.yaml index 4a2b308e670d..6485d4ed57fb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py index a802aaa02511..d0840c4c1ed6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -21,6 +21,7 @@ @configclass class CartpoleRGBCameraSceneCfg(CartpoleSceneCfg): + """Configuration for the cartpole environment with RGB camera.""" # add camera to the scene tiled_camera: TiledCameraCfg = TiledCameraCfg( @@ -37,7 +38,6 @@ class CartpoleRGBCameraSceneCfg(CartpoleSceneCfg): @configclass class CartpoleDepthCameraSceneCfg(CartpoleSceneCfg): - # add camera to the scene tiled_camera: TiledCameraCfg = TiledCameraCfg( prim_path="{ENV_REGEX_NS}/Camera", diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py index f452efda2760..788920af058c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/__init__.py index 969b652ce53b..155079c558f6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/rewards.py index ceb3956996cf..5500089d7f94 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import wrap_to_pi diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/symmetry.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/symmetry.py index 5bf81c900578..3997d2ae1395 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/symmetry.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/cartpole/mdp/symmetry.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import torch from tensordict import TensorDict -from typing import TYPE_CHECKING if TYPE_CHECKING: - from omni.isaac.lab.envs import ManagerBasedRLEnv + from isaaclab.envs import ManagerBasedRLEnv # specify the functions that are available for import __all__ = ["compute_symmetric_states"] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/__init__.py index e3aeb96d4452..67c17ab3bf34 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rl_games_ppo_cfg.yaml index 8774abaca1cc..efb7a8afef89 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rl_games_ppo_cfg.yaml @@ -1,8 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 +# ========================================= IMPORTANT NOTICE ========================================= +# +# This file defines the agent configuration used to generate the "Training Performance" table in +# https://isaac-sim.github.io/IsaacLab/main/source/overview/reinforcement-learning/rl_frameworks.html. +# Ensure that the configurations for the other RL libraries are updated if this one is modified. +# +# ==================================================================================================== + params: seed: 42 @@ -50,13 +58,13 @@ params: device_name: 'cuda:0' multi_gpu: False ppo: True - mixed_precision: True + mixed_precision: False normalize_input: True normalize_value: True value_bootstrap: True num_actors: -1 reward_shaper: - scale_value: 0.6 + scale_value: 1.0 normalize_advantage: True gamma: 0.99 tau: 0.95 @@ -72,7 +80,7 @@ params: truncate_grads: True e_clip: 0.2 horizon_length: 32 - minibatch_size: 32768 + minibatch_size: 32768 # num_envs * horizon_length / num_mini_batches mini_epochs: 5 critic_coef: 4 clip_value: True diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rsl_rl_ppo_cfg.py index 663012f94f03..f2c7f48e4558 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/rsl_rl_ppo_cfg.py @@ -1,8 +1,18 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 +""" +========================================= IMPORTANT NOTICE ========================================= + +This file defines the agent configuration used to generate the "Training Performance" table in +https://isaac-sim.github.io/IsaacLab/main/source/overview/reinforcement-learning/rl_frameworks.html. +Ensure that the configurations for the other RL libraries are updated if this one is modified. + +==================================================================================================== +""" + from isaaclab.utils import configclass from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg @@ -12,18 +22,18 @@ class HumanoidPPORunnerCfg(RslRlOnPolicyRunnerCfg): num_steps_per_env = 32 max_iterations = 1000 - save_interval = 50 + save_interval = 100 experiment_name = "humanoid" policy = RslRlPpoActorCriticCfg( init_noise_std=1.0, - actor_obs_normalization=False, - critic_obs_normalization=False, + actor_obs_normalization=True, + critic_obs_normalization=True, actor_hidden_dims=[400, 200, 100], critic_hidden_dims=[400, 200, 100], activation="elu", ) algorithm = RslRlPpoAlgorithmCfg( - value_loss_coef=1.0, + value_loss_coef=2.0, use_clipped_value_loss=True, clip_param=0.2, entropy_coef=0.0, diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/sb3_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/sb3_ppo_cfg.yaml index 73e4e87c6e41..cd8fd9887416 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/sb3_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/sb3_ppo_cfg.yaml @@ -1,9 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 -# Adapted from rsl_rl config +# ========================================= IMPORTANT NOTICE ========================================= +# +# This file defines the agent configuration used to generate the "Training Performance" table in +# https://isaac-sim.github.io/IsaacLab/main/source/overview/reinforcement-learning/rl_frameworks.html. +# Ensure that the configurations for the other RL libraries are updated if this one is modified. +# +# ==================================================================================================== + seed: 42 policy: "MlpPolicy" n_timesteps: !!float 5e7 @@ -18,7 +25,7 @@ clip_range: 0.2 n_epochs: 5 gae_lambda: 0.95 max_grad_norm: 1.0 -vf_coef: 0.5 +vf_coef: 2.0 policy_kwargs: activation_fn: 'nn.ELU' net_arch: [400, 200, 100] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/skrl_ppo_cfg.yaml index e9f3913a029b..468421e4253f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/agents/skrl_ppo_cfg.yaml @@ -1,8 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 +# ========================================= IMPORTANT NOTICE ========================================= +# +# This file defines the agent configuration used to generate the "Training Performance" table in +# https://isaac-sim.github.io/IsaacLab/main/source/overview/reinforcement-learning/rl_frameworks.html. +# Ensure that the configurations for the other RL libraries are updated if this one is modified. +# +# ==================================================================================================== + seed: 42 @@ -67,14 +75,13 @@ agent: entropy_loss_scale: 0.0 value_loss_scale: 2.0 kl_threshold: 0.0 - rewards_shaper_scale: 0.6 time_limit_bootstrap: False # logging and checkpoint experiment: directory: "humanoid" experiment_name: "" - write_interval: auto - checkpoint_interval: auto + write_interval: 32 + checkpoint_interval: 3200 # Sequential trainer diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py index b622f10433ee..37b9426df9b6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/humanoid_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/__init__.py index bc4fcd6ff800..9fd05f556350 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/observations.py index 308d46532c23..123c4eb7de34 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation from isaaclab.managers import SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/rewards.py index ca6d6b2c08b4..51b47d11449e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/classic/humanoid/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils from isaaclab.assets import Articulation diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/__init__.py new file mode 100644 index 000000000000..0fbb15561904 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/__init__.py @@ -0,0 +1,6 @@ +# 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 + +"""Drone ARL environments.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/__init__.py new file mode 100644 index 000000000000..bc4d65f5b1f2 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/__init__.py @@ -0,0 +1,14 @@ +# 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 sub-module contains the functions that are specific to the drone ARL environments.""" + +from isaaclab.envs.mdp import * # noqa: F401, F403 + +from isaaclab_contrib.mdp import * # noqa: F401, F403 + +from .commands import * # noqa: F401, F403 +from .observations import * # noqa: F401, F403 +from .rewards import * # noqa: F401, F403 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/__init__.py new file mode 100644 index 000000000000..a7386d3ce539 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/__init__.py @@ -0,0 +1,9 @@ +# 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 + +"""Various command terms that can be used in the environment.""" + +from .commands_cfg import DroneUniformPoseCommandCfg +from .drone_pose_command import DroneUniformPoseCommand diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/commands_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/commands_cfg.py new file mode 100644 index 000000000000..f12cf1be082f --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/commands_cfg.py @@ -0,0 +1,16 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.envs.mdp.commands.commands_cfg import UniformPoseCommandCfg +from isaaclab.utils import configclass + +from .drone_pose_command import DroneUniformPoseCommand + + +@configclass +class DroneUniformPoseCommandCfg(UniformPoseCommandCfg): + """Configuration for uniform drone pose command generator.""" + + class_type: type = DroneUniformPoseCommand diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/drone_pose_command.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/drone_pose_command.py new file mode 100644 index 000000000000..f33aa41be4c9 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/commands/drone_pose_command.py @@ -0,0 +1,67 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module containing command generators for pose tracking.""" + +from __future__ import annotations + +import torch + +from isaaclab.envs.mdp.commands.pose_command import UniformPoseCommand +from isaaclab.utils.math import combine_frame_transforms, compute_pose_error + + +class DroneUniformPoseCommand(UniformPoseCommand): + """Drone-specific UniformPoseCommand extensions. + + This class customizes the generic :class:`UniformPoseCommand` for drone (multirotor) + use-cases. Main differences and additions: + + - Transforms pose commands from the drone's base frame to the world frame before use. + - Accounts for per-environment origin offsets (``scene.env_origins``) when computing + position errors so tasks running on shifted/sub-terrain environments compute + meaningful errors. + - Computes and exposes simple metrics used by higher-level code: ``position_error`` + and ``orientation_error`` (stored in ``self.metrics``). + - Provides a debug visualization callback that renders the goal pose (with + sub-terrain shift) and current body pose using the existing visualizers. + + The implementation overrides :meth:`_update_metrics` and :meth:`_debug_vis_callback` + from the base class to implement these drone-specific behaviors. + """ + + def _update_metrics(self): + # transform command from base frame to simulation world frame + self.pose_command_w[:, :3], self.pose_command_w[:, 3:] = combine_frame_transforms( + self.robot.data.root_pos_w, + self.robot.data.root_quat_w, + self.pose_command_b[:, :3], + self.pose_command_b[:, 3:], + ) + # compute the error + pos_error, rot_error = compute_pose_error( + # Sub-terrain shift for correct position error calculation @grzemal + self.pose_command_b[:, :3] + self._env.scene.env_origins, + self.pose_command_w[:, 3:], + self.robot.data.body_pos_w[:, self.body_idx], + self.robot.data.body_quat_w[:, self.body_idx], + ) + self.metrics["position_error"] = torch.norm(pos_error, dim=-1) + self.metrics["orientation_error"] = torch.norm(rot_error, dim=-1) + + def _debug_vis_callback(self, event): + # check if robot is initialized + # note: this is needed in-case the robot is de-initialized. we can't access the data + if not self.robot.is_initialized: + return + # update the markers + # -- goal pose + # Sub-terrain shift for visualization purposes @grzemal + self.goal_pose_visualizer.visualize( + self.pose_command_b[:, :3] + self._env.scene.env_origins, self.pose_command_b[:, 3:] + ) + # -- current body pose + body_link_pose_w = self.robot.data.body_link_pose_w[:, self.body_idx] + self.current_pose_visualizer.visualize(body_link_pose_w[:, :3], body_link_pose_w[:, 3:7]) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/observations.py new file mode 100644 index 000000000000..866eb04e00d5 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/observations.py @@ -0,0 +1,102 @@ +# 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 + +"""Common functions that can be used to create drone observation terms. + +The functions can be passed to the :class:`isaaclab.managers.ObservationTermCfg` object to enable +the observation introduced by the function. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +import torch.jit + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation +from isaaclab.managers import SceneEntityCfg + +from isaaclab_contrib.assets import Multirotor + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv + +from isaaclab.envs.utils.io_descriptors import generic_io_descriptor, record_shape + +""" +State. +""" + + +def base_roll_pitch(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Return the base roll and pitch in the simulation world frame. + + Parameters: + env: Manager-based environment providing the scene and tensors. + asset_cfg: Scene entity config pointing to the target robot (default: "robot"). + + Returns: + torch.Tensor: Shape (num_envs, 2). Column 0 is roll, column 1 is pitch. + Values are radians normalized to [-pi, pi], expressed in the world frame. + + Notes: + - Euler angles are computed from asset.data.root_quat_w using XYZ convention. + - Only roll and pitch are returned; yaw is omitted. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # extract euler angles (in world frame) + roll, pitch, _ = math_utils.euler_xyz_from_quat(asset.data.root_quat_w) + # normalize angle to [-pi, pi] + roll = math_utils.wrap_to_pi(roll) + pitch = math_utils.wrap_to_pi(pitch) + + return torch.cat((roll.unsqueeze(-1), pitch.unsqueeze(-1)), dim=-1) + + +""" +Commands. +""" + + +@generic_io_descriptor(dtype=torch.float32, observation_type="Command", on_inspect=[record_shape]) +def generated_drone_commands( + env: ManagerBasedRLEnv, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Generate a body-frame direction and distance to the commanded position. + + This observation reads a command from env.command_manager identified by command_name, + interprets its first three components as a target position in the world frame, and + returns: + [dir_x, dir_y, dir_z, distance] + where dir_* is the unit vector from the current body origin to the target, expressed + in the multirotor body (root link) frame, and distance is the Euclidean separation. + + Parameters: + env: Manager-based RL environment providing scene and command manager. + command_name: Name of the command term to query from the command manager. + asset_cfg: Scene entity config for the multirotor asset (default: "robot"). + + Returns: + torch.Tensor: Shape (num_envs, 4) with body-frame unit direction (3) and distance (1). + + Frame conventions: + - Current position is asset.data.root_pos_w relative to env.scene.env_origins (world frame). + - Body orientation uses asset.data.root_link_quat_w to rotate world vectors into the body frame. + + Assumptions: + - env.command_manager.get_command(command_name) returns at least three values + representing a world-frame target position per environment. + - A small epsilon (1e-8) is used to guard against zero-length direction vectors. + """ + asset: Multirotor = env.scene[asset_cfg.name] + current_position_w = asset.data.root_pos_w - env.scene.env_origins + command = env.command_manager.get_command(command_name) + current_position_b = math_utils.quat_apply_inverse(asset.data.root_link_quat_w, command[:, :3] - current_position_w) + current_position_b_dir = current_position_b / (torch.norm(current_position_b, dim=-1, keepdim=True) + 1e-8) + current_position_b_mag = torch.norm(current_position_b, dim=-1, keepdim=True) + return torch.cat((current_position_b_dir, current_position_b_mag), dim=-1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/rewards.py new file mode 100644 index 000000000000..4ad040563a42 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/mdp/rewards.py @@ -0,0 +1,147 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + +from isaaclab.assets import RigidObject +from isaaclab.managers import SceneEntityCfg + +""" +Drone control rewards. +""" + + +def distance_to_goal_exp( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + std: float = 1.0, + command_name: str = "target_pose", +) -> torch.Tensor: + """Reward the distance to a goal position using an exponential kernel. + + This reward computes an exponential falloff of the squared Euclidean distance + between the commanded target position and the asset (robot) root position. + + Args: + env: The manager-based RL environment instance. + asset_cfg: SceneEntityCfg identifying the asset (defaults to "robot"). + std: Standard deviation used in the exponential kernel; larger values + produce a gentler falloff. + command_name: Name of the command to read the target pose from the + environment's command manager. The function expects the command + tensor to contain positions in its first three columns. + + Returns: + A 1-D tensor of shape (num_envs,) containing the per-environment reward + values in [0, 1], with 1.0 when the position error is zero. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + command = env.command_manager.get_command(command_name) + + target_position_w = command[:, :3].clone() + current_position = asset.data.root_pos_w - env.scene.env_origins + + # compute the error + position_error_square = torch.sum(torch.square(target_position_w - current_position), dim=1) + return torch.exp(-position_error_square / std**2) + + +def ang_vel_xyz_exp( + env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), std: float = 1.0 +) -> torch.Tensor: + """Penalize angular velocity magnitude with an exponential kernel. + + This reward computes exp(-||omega||^2 / std^2) where omega is the body-frame + angular velocity of the asset. It is useful for encouraging low rotational + rates. + + Args: + env: The manager-based RL environment instance. + asset_cfg: SceneEntityCfg identifying the asset (defaults to "robot"). + std: Standard deviation used in the exponential kernel; controls + sensitivity to angular velocity magnitude. + + Returns: + A 1-D tensor of shape (num_envs,) with values in (0, 1], where 1 indicates + zero angular velocity. + """ + + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + + # compute squared magnitude of angular velocity (all axes) + ang_vel_squared = torch.sum(torch.square(asset.data.root_ang_vel_b), dim=1) + + return torch.exp(-ang_vel_squared / std**2) + + +def lin_vel_xyz_exp( + env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), std: float = 1.0 +) -> torch.Tensor: + """Penalize linear velocity magnitude with an exponential kernel. + + Computes exp(-||v||^2 / std^2) where v is the asset's linear velocity in + world frame. Useful for encouraging the agent to reduce translational speed. + + Args: + env: The manager-based RL environment instance. + asset_cfg: SceneEntityCfg identifying the asset (defaults to "robot"). + std: Standard deviation used in the exponential kernel. + + Returns: + A 1-D tensor of shape (num_envs,) with values in (0, 1], where 1 indicates + zero linear velocity. + """ + + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + + # compute squared magnitude of linear velocity (all axes) + lin_vel_squared = torch.sum(torch.square(asset.data.root_lin_vel_w), dim=1) + + return torch.exp(-lin_vel_squared / std**2) + + +def yaw_aligned( + env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), std: float = 0.5 +) -> torch.Tensor: + """Reward alignment of the vehicle's yaw to zero using an exponential kernel. + + The function extracts the yaw (rotation about Z) from the world-frame root + quaternion and computes exp(-yaw^2 / std^2). This encourages heading + alignment to a zero-yaw reference. + + Args: + env: The manager-based RL environment instance. + asset_cfg: SceneEntityCfg identifying the asset (defaults to "robot"). + std: Standard deviation used in the exponential kernel; smaller values + make the reward more sensitive to yaw deviations. + + Returns: + A 1-D tensor of shape (num_envs,) with values in (0, 1], where 1 indicates + perfect yaw alignment (yaw == 0). + """ + + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + + # extract yaw from current orientation + _, _, yaw = math_utils.euler_xyz_from_quat(asset.data.root_quat_w) + + # normalize yaw to [-pi, pi] (target is 0) + yaw = math_utils.wrap_to_pi(yaw) + + # return exponential reward (1 when yaw=0, approaching 0 when rotated) + return torch.exp(-(yaw**2) / std**2) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/__init__.py new file mode 100644 index 000000000000..36cc6105ac85 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/__init__.py @@ -0,0 +1,6 @@ +# 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 + +"""Drone ARL state-based control environments.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/__init__.py new file mode 100644 index 000000000000..5d69799b587f --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for state-based control environments.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/__init__.py new file mode 100644 index 000000000000..0dddd02bc027 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/__init__.py @@ -0,0 +1,36 @@ +# 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 gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +gym.register( + id="Isaac-TrackPositionNoObstacles-ARL-Robot-1-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.no_obstacle_env_cfg:NoObstacleEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:TrackPositionNoObstaclesEnvPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-TrackPositionNoObstacles-ARL-Robot-1-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.no_obstacle_env_cfg:NoObstacleEnvCfg_PLAY", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:TrackPositionNoObstaclesEnvPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + }, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 000000000000..d454c4caee22 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,87 @@ +# 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 + +params: + seed: 42 + + # environment wrapper clipping + env: + clip_actions: 1.0 + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [256,128,64] + d2rl: False + activation: elu + initializer: + name: default + scale: 2 + rnn: + name: gru + units: 64 + layers: 1 + # before_mlp: False + # layer_norm: True + config: + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + env_config: + num_envs: 8192 + + name: arl_robot_1_track_position_state_based + reward_shaper: + # min_val: -1 + scale_value: 0.1 + + normalize_advantage: True + gamma: 0.98 + tau: 0.95 + ppo: True + learning_rate: 1e-4 + lr_schedule: adaptive + kl_threshold: 0.016 + save_best_after: 10 + score_to_win: 100000 + grad_norm: 1.0 + entropy_coef: 0 + truncate_grads: True + e_clip: 0.2 + clip_value: False + num_actors: 1024 + horizon_length: 32 + minibatch_size: 2048 + mini_epochs: 4 + critic_coef: 2 + normalize_input: True + bounds_loss_coef: 0.0001 + max_epochs: 1500 + normalize_value: True + use_diagnostics: True + value_bootstrap: True + #weight_decay: 0.0001 + use_smooth_clamp: False + + player: + render: True + deterministic: True + games_num: 100000 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 000000000000..b53c53dbdd0c --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,37 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class TrackPositionNoObstaclesEnvPPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 24 + max_iterations = 1500 + save_interval = 50 + experiment_name = "arl_robot_1_track_position_state_based" + empirical_normalization = False + policy = RslRlPpoActorCriticCfg( + init_noise_std=0.5, + actor_hidden_dims=[256, 128, 64], + critic_hidden_dims=[256, 128, 64], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.001, + num_learning_epochs=4, + num_mini_batches=4, + learning_rate=4.0e-4, + schedule="adaptive", + gamma=0.98, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/skrl_ppo_cfg.yaml new file mode 100644 index 000000000000..3a5779e0106a --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/agents/skrl_ppo_cfg.yaml @@ -0,0 +1,95 @@ +# 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 + +seed: 42 + + +# Models are instantiated using skrl's model instantiator utility +# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html +models: + separate: False + policy: + class: GaussianMixin + clip_actions: False + clip_log_std: True + min_log_std: -20.0 + max_log_std: 2.0 + initial_log_std: 0.0 + network: + - name: mlp + input: STATES + layers: [256, 128, 64] + activations: elu + - name: gru + input: mlp + type: GRU + layers: [64] + num_layers: 1 + output: ACTIONS + value: + class: DeterministicMixin + clip_actions: False + network: + - name: mlp + input: STATES + layers: [256, 128, 64] + activations: elu + - name: gru + input: mlp + type: GRU + layers: [64] + num_layers: 1 + output: ONE + + +# Rollout memory +# https://skrl.readthedocs.io/en/latest/api/memories/random.html +memory: + class: RandomMemory + memory_size: -1 # automatically determined (same as agent:rollouts) + + +# PPO agent configuration (field names are from PPO_DEFAULT_CONFIG) +# https://skrl.readthedocs.io/en/latest/api/agents/ppo.html +agent: + class: PPO + rollouts: 24 + learning_epochs: 5 + mini_batches: 4 + discount_factor: 0.99 + lambda: 0.95 + learning_rate: 1.0e-03 + learning_rate_scheduler: KLAdaptiveLR + learning_rate_scheduler_kwargs: + kl_threshold: 0.01 + state_preprocessor: null + state_preprocessor_kwargs: null + value_preprocessor: RunningStandardScaler + value_preprocessor_kwargs: null + random_timesteps: 0 + learning_starts: 0 + grad_norm_clip: 1.0 + ratio_clip: 0.2 + value_clip: 0.2 + clip_predicted_values: True + entropy_loss_scale: 0.005 + value_loss_scale: 1.0 + kl_threshold: 0.0 + rewards_shaper_scale: 0.6 + time_limit_bootstrap: False + # logging and checkpoint + experiment: + directory: "arl_robot_1_track_position_state_based" + experiment_name: "" + write_interval: auto + checkpoint_interval: auto + + +# Sequential trainer +# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html +trainer: + class: SequentialTrainer + timesteps: 36000 + environment_info: log diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/no_obstacle_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/no_obstacle_env_cfg.py new file mode 100644 index 000000000000..92a11d824420 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/no_obstacle_env_cfg.py @@ -0,0 +1,41 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + +from isaaclab_assets.robots.arl_robot_1 import ARL_ROBOT_1_CFG + +from .track_position_state_based_env_cfg import TrackPositionNoObstaclesEnvCfg + +## +# Pre-defined configs +## + + +@configclass +class NoObstacleEnvCfg(TrackPositionNoObstaclesEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + # switch robot to arl_robot_1 + self.scene.robot = ARL_ROBOT_1_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + self.scene.robot.actuators["thrusters"].dt = self.sim.dt + + +@configclass +class NoObstacleEnvCfg_PLAY(NoObstacleEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + + # make a smaller scene for play + self.scene.num_envs = 50 + self.scene.env_spacing = 2.5 + + # disable randomization for play + self.observations.policy.enable_corruption = False + # remove random pushing event + self.events.base_external_force_torque = None + self.events.push_robot = None diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/track_position_state_based_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/track_position_state_based_env_cfg.py new file mode 100644 index 000000000000..238ca65b5ef8 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/drone_arl/track_position_state_based/config/arl_robot_1/track_position_state_based_env_cfg.py @@ -0,0 +1,229 @@ +# 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 math +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +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 RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +from isaaclab_contrib.assets import MultirotorCfg + +import isaaclab_tasks.manager_based.drone_arl.mdp as mdp + + +## +# Scene definition +## +@configclass +class MySceneCfg(InteractiveSceneCfg): + """Configuration for the terrain scene with a flying robot.""" + + # robots + robot: MultirotorCfg = MISSING + + # lights + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=750.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + +## +# MDP settings +## + + +@configclass +class CommandsCfg: + """Command specifications for the MDP.""" + + target_pose = mdp.DroneUniformPoseCommandCfg( + asset_name="robot", + body_name="base_link", + resampling_time_range=(10.0, 10.0), + debug_vis=True, + ranges=mdp.DroneUniformPoseCommandCfg.Ranges( + pos_x=(-0.0, 0.0), + pos_y=(-0.0, 0.0), + pos_z=(-0.0, 0.0), + roll=(-0.0, 0.0), + pitch=(-0.0, 0.0), + yaw=(-0.0, 0.0), + ), + ) + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + thrust_command = mdp.ThrustActionCfg( + asset_name="robot", + scale=3.0, + offset=3.0, + preserve_order=False, + use_default_offset=False, + clip={ + "back_left_prop": (0.0, 6.0), + "back_right_prop": (0.0, 6.0), + "front_left_prop": (0.0, 6.0), + "front_right_prop": (0.0, 6.0), + }, + ) + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + # observation terms (order preserved) + base_link_position = ObsTerm(func=mdp.root_pos_w, noise=Unoise(n_min=-0.1, n_max=0.1)) + base_orientation = ObsTerm(func=mdp.root_quat_w, noise=Unoise(n_min=-0.1, n_max=0.1)) + base_lin_vel = ObsTerm(func=mdp.base_lin_vel, noise=Unoise(n_min=-0.1, n_max=0.1)) + base_ang_vel = ObsTerm(func=mdp.base_ang_vel, noise=Unoise(n_min=-0.1, n_max=0.1)) + last_action = ObsTerm(func=mdp.last_action, noise=Unoise(n_min=-0.0, n_max=0.0)) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + # observation groups + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + # reset + + reset_base = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": { + "x": (-1.0, 1.0), + "y": (-1.0, 1.0), + "z": (-1.0, 1.0), + "yaw": (-math.pi / 6.0, math.pi / 6.0), + "roll": (-math.pi / 6.0, math.pi / 6.0), + "pitch": (-math.pi / 6.0, math.pi / 6.0), + }, + "velocity_range": { + "x": (-0.2, 0.2), + "y": (-0.2, 0.2), + "z": (-0.2, 0.2), + "roll": (-0.2, 0.2), + "pitch": (-0.2, 0.2), + "yaw": (-0.2, 0.2), + }, + }, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + distance_to_goal_exp = RewTerm( + func=mdp.distance_to_goal_exp, + weight=25.0, + params={ + "asset_cfg": SceneEntityCfg("robot"), + "std": 1.5, + "command_name": "target_pose", + }, + ) + flat_orientation_l2 = RewTerm( + func=mdp.flat_orientation_l2, + weight=1.0, + params={"asset_cfg": SceneEntityCfg("robot")}, + ) + yaw_aligned = RewTerm( + func=mdp.yaw_aligned, + weight=2.0, + params={"asset_cfg": SceneEntityCfg("robot"), "std": 1.0}, + ) + lin_vel_xyz_exp = RewTerm( + func=mdp.lin_vel_xyz_exp, + weight=2.5, + params={"asset_cfg": SceneEntityCfg("robot"), "std": 2.0}, + ) + ang_vel_xyz_exp = RewTerm( + func=mdp.ang_vel_xyz_exp, + weight=10.0, + params={"asset_cfg": SceneEntityCfg("robot"), "std": 10.0}, + ) + action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=-0.05) + action_magnitude_l2 = RewTerm(func=mdp.action_l2, weight=-0.05) + + termination_penalty = RewTerm( + func=mdp.is_terminated, + weight=-5.0, + ) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + crash = DoneTerm(func=mdp.root_height_below_minimum, params={"minimum_height": -3.0}) + + +## +# Environment configuration +## + + +@configclass +class TrackPositionNoObstaclesEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the state-based drone pose-control environment.""" + + # Scene settings + scene: MySceneCfg = MySceneCfg(num_envs=4096, env_spacing=2.5) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 10 + self.episode_length_s = 5.0 + # simulation settings + self.sim.dt = 0.01 + self.sim.render_interval = self.decimation + self.sim.physics_material = sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + ) + self.sim.physx.gpu_max_rigid_patch_count = 10 * 2**15 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/__init__.py index 739fdf113e69..72b01368b499 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/__init__.py index a3b30988b7fc..ff7e927b05e8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/action_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/action_cfg.py index 4d8db0b0c150..b195334ba684 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/action_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/action_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/agile_locomotion_observation_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/agile_locomotion_observation_cfg.py index e4e22987442a..6fd0b6dbdf9d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/agile_locomotion_observation_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/agile_locomotion_observation_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/pink_controller_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/pink_controller_cfg.py index 1c80674e3831..488d22c137b8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/pink_controller_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/configs/pink_controller_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/fixed_base_upper_body_ik_g1_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/fixed_base_upper_body_ik_g1_env_cfg.py index e3ace99b5200..87c100a6cec3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/fixed_base_upper_body_ik_g1_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/fixed_base_upper_body_ik_g1_env_cfg.py @@ -1,11 +1,9 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab_assets.robots.unitree import G1_29DOF_CFG - import isaaclab.envs.mdp as base_mdp import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg @@ -27,6 +25,8 @@ from isaaclab_tasks.manager_based.locomanipulation.pick_place import mdp as locomanip_mdp from isaaclab_tasks.manager_based.manipulation.pick_place import mdp as manip_mdp +from isaaclab_assets.robots.unitree import G1_29DOF_CFG + from isaaclab_tasks.manager_based.locomanipulation.pick_place.configs.pink_controller_cfg import ( # isort: skip G1_UPPER_BODY_IK_ACTION_CFG, ) @@ -191,7 +191,7 @@ def __post_init__(self): self.sim.render_interval = 2 # Set the URDF and mesh paths for the IK controller - urdf_omniverse_path = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_kinematics_asset/g1_29dof_with_hand_only_kinematics.urdf" + urdf_omniverse_path = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_kinematics_asset/g1_29dof_with_hand_only_kinematics.urdf" # noqa: E501 # Retrieve local paths for the URDF and mesh files. Will be cached for call after the first time. self.actions.upper_body_ik.controller.urdf_path = retrieve_file_path(urdf_omniverse_path) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/locomanipulation_g1_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/locomanipulation_g1_env_cfg.py index bf09c7f04268..9ff8102fe2e5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/locomanipulation_g1_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/locomanipulation_g1_env_cfg.py @@ -1,20 +1,24 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause - -from isaaclab_assets.robots.unitree import G1_29DOF_CFG - import isaaclab.envs.mdp as base_mdp import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg from isaaclab.devices.device_base import DevicesCfg from isaaclab.devices.openxr import OpenXRDeviceCfg, XrCfg from isaaclab.devices.openxr.retargeters.humanoid.unitree.g1_lower_body_standing import G1LowerBodyStandingRetargeterCfg +from isaaclab.devices.openxr.retargeters.humanoid.unitree.g1_motion_controller_locomotion import ( + G1LowerBodyStandingMotionControllerRetargeterCfg, +) +from isaaclab.devices.openxr.retargeters.humanoid.unitree.trihand.g1_upper_body_motion_ctrl_retargeter import ( + G1TriHandUpperBodyMotionControllerRetargeterCfg, +) from isaaclab.devices.openxr.retargeters.humanoid.unitree.trihand.g1_upper_body_retargeter import ( G1TriHandUpperBodyRetargeterCfg, ) +from isaaclab.devices.openxr.xr_cfg import XrAnchorRotationMode from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.managers import ObservationGroupCfg as ObsGroup from isaaclab.managers import ObservationTermCfg as ObsTerm @@ -32,6 +36,8 @@ ) from isaaclab_tasks.manager_based.manipulation.pick_place import mdp as manip_mdp +from isaaclab_assets.robots.unitree import G1_29DOF_CFG + from isaaclab_tasks.manager_based.locomanipulation.pick_place.configs.pink_controller_cfg import ( # isort: skip G1_UPPER_BODY_IK_ACTION_CFG, ) @@ -202,11 +208,16 @@ def __post_init__(self): self.sim.render_interval = 2 # Set the URDF and mesh paths for the IK controller - urdf_omniverse_path = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_kinematics_asset/g1_29dof_with_hand_only_kinematics.urdf" + urdf_omniverse_path = f"{ISAACLAB_NUCLEUS_DIR}/Controllers/LocomanipulationAssets/unitree_g1_kinematics_asset/g1_29dof_with_hand_only_kinematics.urdf" # noqa: E501 # Retrieve local paths for the URDF and mesh files. Will be cached for call after the first time. self.actions.upper_body_ik.controller.urdf_path = retrieve_file_path(urdf_omniverse_path) + self.xr.anchor_prim_path = "/World/envs/env_0/Robot/pelvis" + self.xr.fixed_anchor_height = True + # Ensure XR anchor rotation follows the robot pelvis (yaw only), with smoothing for comfort + self.xr.anchor_rotation_mode = XrAnchorRotationMode.FOLLOW_PRIM_SMOOTHED + self.teleop_devices = DevicesCfg( devices={ "handtracking": OpenXRDeviceCfg( @@ -225,5 +236,19 @@ def __post_init__(self): sim_device=self.sim.device, xr_cfg=self.xr, ), + "motion_controllers": OpenXRDeviceCfg( + retargeters=[ + G1TriHandUpperBodyMotionControllerRetargeterCfg( + enable_visualization=True, + sim_device=self.sim.device, + hand_joint_names=self.actions.upper_body_ik.hand_joint_names, + ), + G1LowerBodyStandingMotionControllerRetargeterCfg( + sim_device=self.sim.device, + ), + ], + sim_device=self.sim.device, + xr_cfg=self.xr, + ), } ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/__init__.py index 18ec38070d59..7e559309b5cf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/actions.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/actions.py index ad0384a5b821..64d27dbc2f2a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/actions.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/actions.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets.articulation import Articulation from isaaclab.managers.action_manager import ActionTerm from isaaclab.utils.assets import retrieve_file_path diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/observations.py index ab027ce0bf13..d4b3f2b4bdf4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/pick_place/mdp/observations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/__init__.py index df0a1cfdf72f..e952f370f823 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/rsl_rl_ppo_cfg.py index 942a5230f1d7..c98c2030a2ca 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/loco_manip_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/loco_manip_env_cfg.py index 5822ac6487a2..a91ff0907dc7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/loco_manip_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomanipulation/tracking/config/digit/loco_manip_env_cfg.py @@ -1,17 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 math -from isaaclab_assets.robots.agility import ARM_JOINT_NAMES, LEG_JOINT_NAMES - -from isaaclab.managers import EventTermCfg +from isaaclab.managers import EventTermCfg, SceneEntityCfg from isaaclab.managers import ObservationGroupCfg as ObsGroup from isaaclab.managers import ObservationTermCfg as ObsTerm from isaaclab.managers import RewardTermCfg as RewTerm -from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise @@ -20,6 +17,8 @@ from isaaclab_tasks.manager_based.locomotion.velocity.config.digit.rough_env_cfg import DigitRewards, DigitRoughEnvCfg from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import EventCfg +from isaaclab_assets.robots.agility import ARM_JOINT_NAMES, LEG_JOINT_NAMES + @configclass class DigitLocoManipRewards(DigitRewards): @@ -90,6 +89,7 @@ class DigitLocoManipRewards(DigitRewards): @configclass class DigitLocoManipObservations: + """Configuration for the Digit Locomanipulation environment.""" @configclass class PolicyCfg(ObsGroup): @@ -236,7 +236,6 @@ def __post_init__(self): class DigitLocoManipEnvCfg_PLAY(DigitLocoManipEnvCfg): - def __post_init__(self) -> None: super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/__init__.py index 8dee659040a8..5ce57dc1bd56 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/__init__.py index d1e9ae9d88d6..7971b7cd3640 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/__init__.py index fd47c816e0ea..26f3257daef4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/__init__.py index 8a89b4bb5c1a..99ead146751d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/rsl_rl_ppo_cfg.py index db162f1228fc..972ebf937367 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/sb3_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/sb3_ppo_cfg.yaml index e19122141647..cc16b3ce79bd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/sb3_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/sb3_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_flat_ppo_cfg.yaml index 873657e3578a..eeb09d2b8a13 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_rough_ppo_cfg.yaml index b8227096f5d2..e7eff6aa9a83 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/flat_env_cfg.py index 018d33f0a0cf..eb239d363775 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/rough_env_cfg.py index 238e3bd03eac..371ccb5b0cd4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/a1/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py index facb0aaf9502..e0f3eafd39e5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/rsl_rl_ppo_cfg.py index b92ccac2e794..f6d0c585dd15 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_flat_ppo_cfg.yaml index d8c336da407f..4daadf173833 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_rough_ppo_cfg.yaml index 2273df9c37d3..51dd9c727236 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/flat_env_cfg.py index fa24abacda79..134fd0154bf8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/rough_env_cfg.py index ab51e5fd9de3..dd11ad858479 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_b/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py index efcbbe7901dc..39b8e5caeaa8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml index 4bb0f5dfa753..95916252ac26 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml index 375ecc8be278..54a9847bbef2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_ppo_cfg.py index 507f602c3c57..45f434fe7f0d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml index f0942278b837..087eed1e266c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml index 5c7fedf07b00..1baab1c851bc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py index c2d5e51cccde..76ccb79b48ab 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py index 1e457f1f7921..ed62e06fc944 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py index 05fa5ca36f30..41b0398e5206 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_distillation_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_distillation_cfg.py index fd68b9a8959e..ea3d5f521ac3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_distillation_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_distillation_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_ppo_cfg.py index c5b2c1c1848d..220efdd6e8c4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_flat_ppo_cfg.yaml index 88a2bc75b25f..f5510e337706 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_rough_ppo_cfg.yaml index 9df85573ef5e..a612f624db17 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/flat_env_cfg.py index 9e857e4de15d..7abab44fdb9b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/rough_env_cfg.py index 929bbeeaaaa9..c672dcacc0ce 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/anymal_d/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py index c24e3d8fa40d..7cb2be156961 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/rsl_rl_ppo_cfg.py index 719f8a241051..93cce1bb9294 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_flat_ppo_cfg.yaml index dd80f5fd1965..0f55bd81a18c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_rough_ppo_cfg.yaml index 883148f878ec..ddd65baaa3ab 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/flat_env_cfg.py index f58b99731135..5ca23455cd0e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/rough_env_cfg.py index f99f36f642d6..2a13f35213ca 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/cassie/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/__init__.py index 1f9915fb27e5..8311b225698d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/rsl_rl_ppo_cfg.py index 00be11a490f7..72eb4a2aa3ff 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/flat_env_cfg.py index 6a47f1529af3..48a647e17a64 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/rough_env_cfg.py index 3d567231df03..792e6f639479 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/digit/rough_env_cfg.py @@ -1,12 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 math -from isaaclab_assets.robots.agility import ARM_JOINT_NAMES, DIGIT_V4_CFG, LEG_JOINT_NAMES - from isaaclab.managers import ObservationGroupCfg, ObservationTermCfg, RewardTermCfg, SceneEntityCfg, TerminationTermCfg from isaaclab.utils import configclass from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise @@ -14,6 +12,8 @@ import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg +from isaaclab_assets.robots.agility import ARM_JOINT_NAMES, DIGIT_V4_CFG, LEG_JOINT_NAMES + @configclass class DigitRewards: diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py index 6ec20c374e9a..30861ec5a3a5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/rsl_rl_ppo_cfg.py index 946490165380..61a6d0261b9f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_flat_ppo_cfg.yaml index b6ecdf1f3013..711b7190245e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_rough_ppo_cfg.yaml index 6013e3f070d3..b54682b45cd1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/flat_env_cfg.py index bdf2f07c07cf..e8d3b5edc451 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py index 4ef14c815422..04971c3d9f20 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/g1/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py index 1faee1480049..def24b8e1442 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/rsl_rl_ppo_cfg.py index 5be515ccc0d6..9baa2b371ea3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_flat_ppo_cfg.yaml index 7cd7c9bb5b5b..d125c913446f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_rough_ppo_cfg.yaml index 79daaec43f2b..47888d623e91 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/flat_env_cfg.py index 54362a6f380f..760c1f5f5d07 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/rough_env_cfg.py index 01236ae7d692..91efcc170245 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go1/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py index c9766e7d3a27..4ea7d3fce718 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/rsl_rl_ppo_cfg.py index e0c6afab9ea6..9777785f7e30 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_flat_ppo_cfg.yaml index 1b3ecf74fd53..e36d3a574867 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_rough_ppo_cfg.yaml index aeffb439a172..4c89ca249f01 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/flat_env_cfg.py index fbcb4b3e522b..8bf8bb1373f9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/rough_env_cfg.py index 22fb69cff442..69e6adddd042 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/go2/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py index dd4adfb1850a..6a218243e371 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/rsl_rl_ppo_cfg.py index 1163ac744c46..102359770864 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_flat_ppo_cfg.yaml index 1bcc39eb42ef..ed1dbeb89d11 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_rough_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_rough_ppo_cfg.yaml index 7538f906a217..c5f49d24efdb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_rough_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/agents/skrl_rough_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/flat_env_cfg.py index 8d436e6b8053..e9b9e2a1fa27 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py index 31864701c47d..799a7b95cc40 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/h1/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py index cec1edcdf533..28572a7dfa5d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py index 951fb421cfce..3985f6b3b491 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/skrl_flat_ppo_cfg.yaml index c380e841e4c0..dcbf8926268b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py index 4b2f5509bab1..6bf334e24536 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -295,6 +295,7 @@ class SpotTerminationsCfg: @configclass class SpotFlatEnvCfg(LocomotionVelocityRoughEnvCfg): + """Configuration for the Spot robot in a flat environment.""" # Basic settings observations: SpotObservationsCfg = SpotObservationsCfg() diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py index eb4c94a1f609..cf460b5f33fe 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/events.py index 7c129495401c..b1a47934d95e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/events.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +12,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import sample_uniform diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/rewards.py index 45c3315f44c9..05680e437355 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/config/spot/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import ManagerTermBase, SceneEntityCfg from isaaclab.sensors import ContactSensor @@ -86,9 +87,10 @@ def base_linear_velocity_reward( class GaitReward(ManagerTermBase): """Gait enforcing reward term for quadrupeds. - This reward penalizes contact timing differences between selected foot pairs defined in :attr:`synced_feet_pair_names` - to bias the policy towards a desired gait, i.e trotting, bounding, or pacing. Note that this reward is only for - quadrupedal gaits with two pairs of synchronized feet. + This reward penalizes contact timing differences between selected foot pairs defined in + :attr:`synced_feet_pair_names` to bias the policy towards a desired gait, i.e trotting, + bounding, or pacing. Note that this reward is only for quadrupedal gaits with two pairs + of synchronized feet. """ def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/__init__.py index a8a1af6d9264..6f6cad007128 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py index 69b7e09b384a..88187a6b816b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +11,11 @@ from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation from isaaclab.managers import SceneEntityCfg from isaaclab.terrains import TerrainImporter diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/rewards.py index 7a1fc12a1dba..f804aa6884c5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.envs import mdp from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import ContactSensor @@ -87,7 +88,9 @@ def feet_slide(env, sensor_cfg: SceneEntityCfg, asset_cfg: SceneEntityCfg = Scen def track_lin_vel_xy_yaw_frame_exp( env, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") ) -> torch.Tensor: - """Reward tracking of linear velocity commands (xy axes) in the gravity aligned robot frame using exponential kernel.""" + """Reward tracking of linear velocity commands (xy axes) in the gravity aligned + robot frame using an exponential kernel. + """ # extract the used quantities (to enable type-hinting) asset = env.scene[asset_cfg.name] vel_yaw = quat_apply_inverse(yaw_quat(asset.data.root_quat_w), asset.data.root_lin_vel_w[:, :3]) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/__init__.py index 027c9900a95c..abbd6c26ca51 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/anymal.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/anymal.py index 7d2db8fa7fff..f4197ccbe76e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/anymal.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/symmetry/anymal.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,12 +8,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import torch from tensordict import TensorDict -from typing import TYPE_CHECKING if TYPE_CHECKING: - from omni.isaac.lab.envs import ManagerBasedRLEnv + from isaaclab.envs import ManagerBasedRLEnv # specify the functions that are available for import __all__ = ["compute_symmetric_states"] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/terminations.py index 833663df1637..6c037d01ea51 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import RigidObject from isaaclab.managers import SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py index 735333f9a49d..d7094e777014 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/__init__.py index 8f5703b9b321..eaf0b09fbb66 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/__init__.py index 93963225e103..7ea0d7159142 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py index b6a3702eab81..85b7e5ae9ba8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/cabinet_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +7,7 @@ from dataclasses import MISSING import isaaclab.sim as sim_utils -from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.managers import EventTermCfg as EventTerm diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/__init__.py index 4bcedc925395..d7c38f5c0b04 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/__init__.py index 8e00700be3e4..1f8b763b4228 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rl_games_ppo_cfg.yaml index 2f0a60d12cfa..8fc9b9773d5d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rsl_rl_ppo_cfg.py index ee642fb07aa8..0ccb4787cdd6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/skrl_ppo_cfg.yaml index 4e81f3673de6..ca95cb45ecec 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_abs_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_abs_env_cfg.py index 6d5105e31db1..2f47f3239580 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_abs_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_abs_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_rel_env_cfg.py index e45666439699..aaaa644ce1c2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/ik_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/joint_pos_env_cfg.py index 046232257dd4..04624c0e6389 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/franka/joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/__init__.py new file mode 100644 index 000000000000..a4630e8b3bfd --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/__init__.py @@ -0,0 +1,38 @@ +# 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 gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +## +# Joint Position Control +## + +gym.register( + id="Isaac-Open-Drawer-OpenArm-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmCabinetEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmCabinetPPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + }, + disable_env_checker=True, +) + +gym.register( + id="Isaac-Open-Drawer-OpenArm-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmCabinetEnvCfg_PLAY", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmCabinetPPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + }, + disable_env_checker=True, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 000000000000..52d5a7dfae87 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,81 @@ +# 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 + +params: + seed: 42 + + # environment wrapper clipping + env: + clip_observations: 5.0 + clip_actions: 1.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + network: + name: actor_critic + separate: False + + space: + continuous: + mu_activation: None + sigma_activation: None + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [256, 128, 64] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False + load_path: '' + + config: + name: openarm_open_drawer + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: False + normalize_value: False + num_actors: -1 # configured from the script (based on num_envs) + reward_shaper: + scale_value: 1 + normalize_advantage: False + gamma: 0.99 + tau: 0.95 + learning_rate: 5e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 200 + max_epochs: 400 + save_best_after: 50 + save_frequency: 50 + print_stats: True + grad_norm: 1.0 + entropy_coef: 0.001 + truncate_grads: True + e_clip: 0.2 + horizon_length: 96 + minibatch_size: 4096 + mini_epochs: 5 + critic_coef: 4 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 000000000000..67f3498c361d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,37 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class OpenArmCabinetPPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 96 + max_iterations = 600 + save_interval = 50 + experiment_name = "openarm_open_drawer" + empirical_normalization = False + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_hidden_dims=[256, 128, 64], + critic_hidden_dims=[256, 128, 64], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=1e-3, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=5.0e-4, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.02, + max_grad_norm=1.0, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/cabinet_openarm_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/cabinet_openarm_env_cfg.py new file mode 100644 index 000000000000..6e3eecb59382 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/cabinet_openarm_env_cfg.py @@ -0,0 +1,282 @@ +# 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 + +""" +We modified parts of the environment, such as the target's position and orientation, +as well as certain object properties, to better suit the smaller robot. +""" + +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.assets import ArticulationCfg, AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +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 RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sensors import FrameTransformerCfg +from isaaclab.sensors.frame_transformer import OffsetCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +## +# Pre-defined configs +## +from isaaclab.markers.config import FRAME_MARKER_CFG # isort: skip + +FRAME_MARKER_SMALL_CFG = FRAME_MARKER_CFG.copy() +FRAME_MARKER_SMALL_CFG.markers["frame"].scale = (0.10, 0.10, 0.10) + +from ... import mdp + +## +# Scene definition +## + + +@configclass +class CabinetSceneCfg(InteractiveSceneCfg): + """Configuration for the cabinet scene with a robot and a cabinet. + + This is the abstract base implementation, the exact scene is defined in the derived classes + which need to set the robot and end-effector frames + """ + + # robots, Will be populated by agent env cfg + robot: ArticulationCfg = MISSING + # End-effector, Will be populated by agent env cfg + ee_frame: FrameTransformerCfg = MISSING + + cabinet = ArticulationCfg( + prim_path="{ENV_REGEX_NS}/Cabinet", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Sektion_Cabinet/sektion_cabinet_instanceable.usd", + activate_contact_sensors=False, + scale=(0.75, 0.75, 0.75), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.7, 0, 0.3), + rot=(0.0, 0.0, 0.0, 1.0), + joint_pos={ + "door_left_joint": 0.0, + "door_right_joint": 0.0, + "drawer_bottom_joint": 0.0, + "drawer_top_joint": 0.0, + }, + ), + actuators={ + "drawers": ImplicitActuatorCfg( + joint_names_expr=["drawer_top_joint", "drawer_bottom_joint"], + effort_limit=87.0, + velocity_limit=100.0, + stiffness=10.0, + damping=1.0, + ), + "doors": ImplicitActuatorCfg( + joint_names_expr=["door_left_joint", "door_right_joint"], + effort_limit=87.0, + velocity_limit=100.0, + stiffness=10.0, + damping=2.5, + ), + }, + ) + + # Frame definitions for the cabinet. + cabinet_frame = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Cabinet/sektion", + debug_vis=True, + visualizer_cfg=FRAME_MARKER_SMALL_CFG.replace(prim_path="/Visuals/CabinetFrameTransformer"), + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Cabinet/drawer_handle_bottom", + name="drawer_handle_bottom", + offset=OffsetCfg( + pos=(0.222, 0.0, 0.005), + rot=(0.5, 0.5, -0.5, -0.5), # align with end-effector frame + ), + ), + ], + ) + + # plane + plane = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(), + spawn=sim_utils.GroundPlaneCfg(), + collision_group=-1, + ) + + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0), + ) + + +## +# MDP settings +## + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + arm_action: mdp.JointPositionActionCfg = MISSING + gripper_action: mdp.BinaryJointPositionActionCfg = MISSING + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + joint_pos = ObsTerm(func=mdp.joint_pos_rel) + joint_vel = ObsTerm(func=mdp.joint_vel_rel) + cabinet_joint_pos = ObsTerm( + func=mdp.joint_pos_rel, + params={"asset_cfg": SceneEntityCfg("cabinet", joint_names=["drawer_bottom_joint"])}, + ) + cabinet_joint_vel = ObsTerm( + func=mdp.joint_vel_rel, + params={"asset_cfg": SceneEntityCfg("cabinet", joint_names=["drawer_bottom_joint"])}, + ) + rel_ee_drawer_distance = ObsTerm(func=mdp.rel_ee_drawer_distance) + + actions = ObsTerm(func=mdp.last_action) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + + # observation groups + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + robot_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": (0.8, 1.25), + "dynamic_friction_range": (0.8, 1.25), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + + cabinet_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("cabinet", body_names="drawer_handle_bottom"), + "static_friction_range": (2.25, 2.5), + "dynamic_friction_range": (2.0, 2.25), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + + reset_all = EventTerm(func=mdp.reset_scene_to_default, mode="reset") + + reset_robot_joints = EventTerm( + func=mdp.reset_joints_by_offset, + mode="reset", + params={ + "position_range": (-0.1, 0.1), + "velocity_range": (0.0, 0.0), + }, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + # 1. Approach the handle + approach_ee_handle = RewTerm(func=mdp.approach_ee_handle, weight=2.0, params={"threshold": 0.2}) + align_ee_handle = RewTerm(func=mdp.align_ee_handle, weight=0.5) + + # 2. Grasp the handle + approach_gripper_handle = RewTerm(func=mdp.approach_gripper_handle, weight=5.0, params={"offset": MISSING}) + align_grasp_around_handle = RewTerm(func=mdp.align_grasp_around_handle, weight=0.125) + grasp_handle = RewTerm( + func=mdp.grasp_handle, + weight=0.5, + params={ + "threshold": 0.03, + "open_joint_pos": MISSING, + "asset_cfg": SceneEntityCfg("robot", joint_names=MISSING), + }, + ) + + # 3. Open the drawer + open_drawer_bonus = RewTerm( + func=mdp.open_drawer_bonus, + weight=7.5, + params={"asset_cfg": SceneEntityCfg("cabinet", joint_names=["drawer_bottom_joint"])}, + ) + multi_stage_open_drawer = RewTerm( + func=mdp.multi_stage_open_drawer, + weight=1.0, + params={"asset_cfg": SceneEntityCfg("cabinet", joint_names=["drawer_bottom_joint"])}, + ) + + # 4. Penalize actions for cosmetic reasons + action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=-1e-2) + joint_vel = RewTerm(func=mdp.joint_vel_l2, weight=-0.0001) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + +## +# Environment configuration +## + + +@configclass +class CabinetEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the cabinet environment.""" + + # Scene settings + scene: CabinetSceneCfg = CabinetSceneCfg(num_envs=4096, env_spacing=2.0) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 1 + self.episode_length_s = 8.0 + self.viewer.eye = (-2.0, 2.0, 2.0) + self.viewer.lookat = (0.8, 0.0, 0.5) + # simulation settings + self.sim.dt = 1 / 60 # 60Hz + self.sim.render_interval = self.decimation + self.sim.physx.bounce_threshold_velocity = 0.01 + self.sim.physx.friction_correlation_distance = 0.00625 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/joint_pos_env_cfg.py new file mode 100644 index 000000000000..05d03942700e --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/config/openarm/joint_pos_env_cfg.py @@ -0,0 +1,93 @@ +# 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 + +## +# Pre-defined configs +## +from isaaclab.sensors import FrameTransformerCfg +from isaaclab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from isaaclab.utils import configclass + +from isaaclab_tasks.manager_based.manipulation.cabinet import mdp + +from isaaclab_assets.robots.openarm import OPENARM_UNI_CFG + +from isaaclab_tasks.manager_based.manipulation.cabinet.config.openarm.cabinet_openarm_env_cfg import ( # isort: skip + FRAME_MARKER_SMALL_CFG, + CabinetEnvCfg, +) + + +@configclass +class OpenArmCabinetEnvCfg(CabinetEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Set OpenArm as robot + self.scene.robot = OPENARM_UNI_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Set Actions for the specific robot type (OpenArm) + self.actions.arm_action = mdp.JointPositionActionCfg( + asset_name="robot", + joint_names=["openarm_joint.*"], + scale=1.0, + use_default_offset=True, + ) + self.actions.gripper_action = mdp.BinaryJointPositionActionCfg( + asset_name="robot", + joint_names=["openarm_finger_joint.*"], + open_command_expr={"openarm_finger_joint.*": 0.044}, + close_command_expr={"openarm_finger_joint.*": 0.0}, + ) + + # Listens to the required transforms + # IMPORTANT: The order of the frames in the list is important. The first frame is the tool center point (TCP) + # the other frames are the fingers + self.scene.ee_frame = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/openarm_link0", + visualizer_cfg=FRAME_MARKER_SMALL_CFG.replace(prim_path="/Visuals/EndEffectorFrameTransformer"), + debug_vis=False, + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/openarm_ee_tcp", + name="ee_tcp", + offset=OffsetCfg( + pos=(0.0, 0.0, -0.003), + ), + ), + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/openarm_left_finger", + name="tool_leftfinger", + offset=OffsetCfg( + pos=(0.0, -0.005, 0.075), + ), + ), + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/openarm_right_finger", + name="tool_rightfinger", + offset=OffsetCfg( + pos=(0.0, 0.005, 0.075), + ), + ), + ], + ) + + # override rewards + self.rewards.approach_gripper_handle.params["offset"] = 0.04 + self.rewards.grasp_handle.params["open_joint_pos"] = 0.044 + self.rewards.grasp_handle.params["asset_cfg"].joint_names = ["openarm_finger_joint.*"] + + +@configclass +class OpenArmCabinetEnvCfg_PLAY(OpenArmCabinetEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + # make a smaller scene for play + self.scene.num_envs = 50 + self.scene.env_spacing = 2.5 + # disable randomization for play + self.observations.policy.enable_corruption = False diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/__init__.py index c981b9403be2..79a9af2f736b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/observations.py index 164cc026779c..66fb8bb38e97 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import ArticulationData from isaaclab.sensors import FrameTransformerData diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/rewards.py index f8a2c3ff4bbf..433a2a87732a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/cabinet/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import matrix_from_quat diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/__init__.py index eceb73b9ca1f..61fccf6b2c14 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/__init__.py new file mode 100644 index 000000000000..a7b2bf171a20 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2025-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 + +"""Assemble 3 gears into a base.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/__init__.py new file mode 100644 index 000000000000..177e08ed7349 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for arm-based gear assembly environments.""" + +# We leave this file empty since we don't want to expose any configs in this package directly. +# We still need this file to import the "config" module in the parent package. diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/__init__.py new file mode 100644 index 000000000000..ad5dff78db64 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/__init__.py @@ -0,0 +1,75 @@ +# Copyright (c) 2025-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 gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + + +# UR10e with 2F-140 gripper +gym.register( + id="Isaac-Deploy-GearAssembly-UR10e-2F140-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:UR10e2F140GearAssemblyEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UR10GearAssemblyRNNPPORunnerCfg", + }, +) + +gym.register( + id="Isaac-Deploy-GearAssembly-UR10e-2F140-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:UR10e2F140GearAssemblyEnvCfg_PLAY", + }, +) + +# UR10e with 2F-85 gripper +gym.register( + id="Isaac-Deploy-GearAssembly-UR10e-2F85-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:UR10e2F85GearAssemblyEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UR10GearAssemblyRNNPPORunnerCfg", + }, +) + +gym.register( + id="Isaac-Deploy-GearAssembly-UR10e-2F85-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:UR10e2F85GearAssemblyEnvCfg_PLAY", + }, +) + +# UR10e with 2F-140 gripper - ROS Inference +gym.register( + id="Isaac-Deploy-GearAssembly-UR10e-2F140-ROS-Inference-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.ros_inference_env_cfg:UR10e2F140GearAssemblyROSInferenceEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UR10GearAssemblyRNNPPORunnerCfg", + }, +) + +# UR10e with 2F-85 gripper - ROS Inference +gym.register( + id="Isaac-Deploy-GearAssembly-UR10e-2F85-ROS-Inference-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.ros_inference_env_cfg:UR10e2F85GearAssemblyROSInferenceEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:UR10GearAssemblyRNNPPORunnerCfg", + }, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/agents/__init__.py new file mode 100644 index 000000000000..cf59b16a1e2e --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 000000000000..ac1ecba8463d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,49 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticRecurrentCfg, RslRlPpoAlgorithmCfg + + +@configclass +class UR10GearAssemblyRNNPPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 512 + max_iterations = 1500 + save_interval = 50 + experiment_name = "gear_assembly_ur10e" + clip_actions = 1.0 + resume = False + obs_groups = { + "policy": ["policy"], + "critic": ["critic"], + } + policy = RslRlPpoActorCriticRecurrentCfg( + state_dependent_std=True, + init_noise_std=1.0, + actor_obs_normalization=True, + critic_obs_normalization=True, + actor_hidden_dims=[256, 128, 64], + critic_hidden_dims=[256, 128, 64], + noise_std_type="log", + activation="elu", + rnn_type="lstm", + rnn_hidden_dim=256, + rnn_num_layers=2, + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.0, + num_learning_epochs=8, + num_mini_batches=16, + learning_rate=5.0e-4, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.008, + max_grad_norm=1.0, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/joint_pos_env_cfg.py new file mode 100644 index 000000000000..22921e717895 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/joint_pos_env_cfg.py @@ -0,0 +1,520 @@ +# Copyright (c) 2025-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 math + +import torch + +import isaaclab.sim as sim_utils +from isaaclab.actuators import ImplicitActuatorCfg +from isaaclab.assets import ArticulationCfg +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.utils import configclass + +import isaaclab_tasks.manager_based.manipulation.deploy.mdp as mdp +import isaaclab_tasks.manager_based.manipulation.deploy.mdp.events as gear_assembly_events +from isaaclab_tasks.manager_based.manipulation.deploy.gear_assembly.gear_assembly_env_cfg import GearAssemblyEnvCfg + +## +# Pre-defined configs +## +from isaaclab_assets.robots.universal_robots import UR10e_ROBOTIQ_GRIPPER_CFG, UR10e_ROBOTIQ_2F_85_CFG # isort: skip + + +## +# Gripper-specific helper functions +## + + +def set_finger_joint_pos_robotiq_2f140( + joint_pos: torch.Tensor, + reset_ind_joint_pos: list[int], + finger_joints: list[int], + finger_joint_position: float, +): + """Set finger joint positions for Robotiq 2F-140 gripper. + + Args: + joint_pos: Joint positions tensor + reset_ind_joint_pos: Row indices into the sliced joint_pos tensor + finger_joints: List of finger joint indices + finger_joint_position: Target position for finger joints + """ + for idx in reset_ind_joint_pos: + # For 2F-140 gripper (8 joints expected) + # Joint structure: [finger_joint, finger_joint, outer_joints x2, inner_finger_joints x2, pad_joints x2] + if len(finger_joints) < 8: + raise ValueError(f"2F-140 gripper requires at least 8 finger joints, got {len(finger_joints)}") + + joint_pos[idx, finger_joints[0]] = finger_joint_position + joint_pos[idx, finger_joints[1]] = finger_joint_position + + # outer finger joints set to 0 + joint_pos[idx, finger_joints[2]] = 0 + joint_pos[idx, finger_joints[3]] = 0 + + # inner finger joints: multiply by -1 + joint_pos[idx, finger_joints[4]] = -finger_joint_position + joint_pos[idx, finger_joints[5]] = -finger_joint_position + + joint_pos[idx, finger_joints[6]] = finger_joint_position + joint_pos[idx, finger_joints[7]] = finger_joint_position + + +def set_finger_joint_pos_robotiq_2f85( + joint_pos: torch.Tensor, + reset_ind_joint_pos: list[int], + finger_joints: list[int], + finger_joint_position: float, +): + """Set finger joint positions for Robotiq 2F-85 gripper. + + Args: + joint_pos: Joint positions tensor + reset_ind_joint_pos: Row indices into the sliced joint_pos tensor + finger_joints: List of finger joint indices + finger_joint_position: Target position for finger joints + """ + for idx in reset_ind_joint_pos: + # For 2F-85 gripper (6 joints expected) + # Joint structure: [finger_joint, finger_joint, inner_finger_joints x2, inner_finger_knuckle_joints x2] + if len(finger_joints) < 6: + raise ValueError(f"2F-85 gripper requires at least 6 finger joints, got {len(finger_joints)}") + + # Multiply specific indices by -1: [2, 4, 5] + # These correspond to: + # ['left_inner_finger_joint', 'right_inner_finger_knuckle_joint', 'left_inner_finger_knuckle_joint'] + joint_pos[idx, finger_joints[0]] = finger_joint_position + joint_pos[idx, finger_joints[1]] = finger_joint_position + joint_pos[idx, finger_joints[2]] = -finger_joint_position + joint_pos[idx, finger_joints[3]] = finger_joint_position + joint_pos[idx, finger_joints[4]] = -finger_joint_position + joint_pos[idx, finger_joints[5]] = -finger_joint_position + + +## +# Environment configuration +## + + +@configclass +class EventCfg: + """Configuration for events.""" + + robot_joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg( + "robot", joint_names=["shoulder_.*", "elbow_.*", "wrist_.*"] + ), # only the arm joints are randomized + "stiffness_distribution_params": (0.75, 1.5), + "damping_distribution_params": (0.3, 3.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + + joint_friction = EventTerm( + func=mdp.randomize_joint_parameters, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder_.*", "elbow_.*", "wrist_.*"]), + "friction_distribution_params": (0.3, 0.7), + "operation": "add", + "distribution": "uniform", + }, + ) + + small_gear_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("factory_gear_small", body_names=".*"), + "static_friction_range": (0.75, 0.75), + "dynamic_friction_range": (0.75, 0.75), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + + medium_gear_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("factory_gear_medium", body_names=".*"), + "static_friction_range": (0.75, 0.75), + "dynamic_friction_range": (0.75, 0.75), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + + large_gear_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("factory_gear_large", body_names=".*"), + "static_friction_range": (0.75, 0.75), + "dynamic_friction_range": (0.75, 0.75), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + + gear_base_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("factory_gear_base", body_names=".*"), + "static_friction_range": (0.75, 0.75), + "dynamic_friction_range": (0.75, 0.75), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + + robot_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*finger"), + "static_friction_range": (0.75, 0.75), + "dynamic_friction_range": (0.75, 0.75), + "restitution_range": (0.0, 0.0), + "num_buckets": 16, + }, + ) + + randomize_gear_type = EventTerm( + func=gear_assembly_events.randomize_gear_type, + mode="reset", + params={"gear_types": ["gear_small", "gear_medium", "gear_large"]}, + ) + + reset_all = EventTerm(func=mdp.reset_scene_to_default, mode="reset") + + randomize_gears_and_base_pose = EventTerm( + func=gear_assembly_events.randomize_gears_and_base_pose, + mode="reset", + params={ + "pose_range": { + "x": [-0.1, 0.1], + "y": [-0.25, 0.25], + "z": [-0.1, 0.1], + "roll": [-math.pi / 90, math.pi / 90], # 2 degree + "pitch": [-math.pi / 90, math.pi / 90], # 2 degree + "yaw": [-math.pi / 6, math.pi / 6], # 2 degree + }, + "gear_pos_range": { + "x": [-0.02, 0.02], + "y": [-0.02, 0.02], + "z": [0.0575, 0.0775], # 0.045 + 0.0225 + }, + "velocity_range": {}, + }, + ) + + set_robot_to_grasp_pose = EventTerm( + func=gear_assembly_events.set_robot_to_grasp_pose, + mode="reset", + params={ + "robot_asset_cfg": SceneEntityCfg("robot"), + "pos_randomization_range": {"x": [-0.0, 0.0], "y": [-0.005, 0.005], "z": [-0.003, 0.003]}, + }, + ) + + +@configclass +class UR10eGearAssemblyEnvCfg(GearAssemblyEnvCfg): + """Base configuration for UR10e Gear Assembly Environment. + + This class contains common setup shared across different gripper configurations. + Subclasses should configure gripper-specific parameters. + """ + + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Robot-specific parameters (can be overridden for other robots) + self.end_effector_body_name = "wrist_3_link" # End effector body name for IK and termination checks + self.num_arm_joints = 6 # Number of arm joints (excluding gripper) + self.grasp_rot_offset = [ + 0.0, + math.sqrt(2) / 2, + math.sqrt(2) / 2, + 0.0, + ] # Rotation offset for grasp pose (quaternion [w, x, y, z]) + self.gripper_joint_setter_func = None # Gripper-specific joint setter function (set in subclass) + + # Gear orientation termination thresholds (in degrees) + self.gear_orientation_roll_threshold_deg = 15.0 # Maximum allowed roll deviation + self.gear_orientation_pitch_threshold_deg = 15.0 # Maximum allowed pitch deviation + self.gear_orientation_yaw_threshold_deg = 180.0 # Maximum allowed yaw deviation + + # Common observation configuration + self.observations.policy.joint_pos.params["asset_cfg"].joint_names = [ + "shoulder_pan_joint", + "shoulder_lift_joint", + "elbow_joint", + "wrist_1_joint", + "wrist_2_joint", + "wrist_3_joint", + ] + self.observations.policy.joint_vel.params["asset_cfg"].joint_names = [ + "shoulder_pan_joint", + "shoulder_lift_joint", + "elbow_joint", + "wrist_1_joint", + "wrist_2_joint", + "wrist_3_joint", + ] + + # override events + self.events = EventCfg() + + # Update termination thresholds from config + self.terminations.gear_orientation_exceeded.params["roll_threshold_deg"] = ( + self.gear_orientation_roll_threshold_deg + ) + self.terminations.gear_orientation_exceeded.params["pitch_threshold_deg"] = ( + self.gear_orientation_pitch_threshold_deg + ) + self.terminations.gear_orientation_exceeded.params["yaw_threshold_deg"] = ( + self.gear_orientation_yaw_threshold_deg + ) + + # override command generator body + self.joint_action_scale = 0.025 + self.actions.arm_action = mdp.RelativeJointPositionActionCfg( + asset_name="robot", + joint_names=[ + "shoulder_pan_joint", + "shoulder_lift_joint", + "elbow_joint", + "wrist_1_joint", + "wrist_2_joint", + "wrist_3_joint", + ], + scale=self.joint_action_scale, + use_zero_offset=True, + ) + + +@configclass +class UR10e2F140GearAssemblyEnvCfg(UR10eGearAssemblyEnvCfg): + """Configuration for UR10e with Robotiq 2F-140 gripper.""" + + def __post_init__(self): + # post init of parent + super().__post_init__() + + # switch robot to ur10e with 2F-140 gripper + self.scene.robot = UR10e_ROBOTIQ_GRIPPER_CFG.replace( + prim_path="{ENV_REGEX_NS}/Robot", + spawn=UR10e_ROBOTIQ_GRIPPER_CFG.spawn.replace( + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=4, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=1 + ), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + # Joint positions based on IK from center of distribution for randomized gear positions + # This is done so that the start for the differential IK search after randomizing + # is close to the optimal grasp pose + init_state=ArticulationCfg.InitialStateCfg( + joint_pos={ + "shoulder_pan_joint": 2.7228, + "shoulder_lift_joint": -8.3962e-01, + "elbow_joint": 1.3684, + "wrist_1_joint": -2.1048, + "wrist_2_joint": -1.5691, + "wrist_3_joint": -1.9896, + }, + pos=(0.0, 0.0, 0.0), + rot=(1.0, 0.0, 0.0, 0.0), + ), + ) + + # 2F-140 gripper actuator configuration + self.scene.robot.actuators["gripper_finger"] = ImplicitActuatorCfg( + joint_names_expr=[".*_inner_finger_joint"], + effort_limit_sim=10.0, + velocity_limit_sim=10.0, + stiffness=10.0, + damping=0.05, + friction=0.0, + armature=0.0, + ) + + # Set gripper-specific joint setter function + self.gripper_joint_setter_func = set_finger_joint_pos_robotiq_2f140 + + # gear offsets and grasp positions for the 2F-140 gripper + self.gear_offsets_grasp = { + "gear_small": [0.0, self.gear_offsets["gear_small"][0], -0.26], + "gear_medium": [0.0, self.gear_offsets["gear_medium"][0], -0.26], + "gear_large": [0.0, self.gear_offsets["gear_large"][0], -0.26], + } + + # Grasp widths for 2F-140 gripper + self.hand_grasp_width = {"gear_small": 0.64, "gear_medium": 0.54, "gear_large": 0.51} + + # Close widths for 2F-140 gripper + self.hand_close_width = {"gear_small": 0.69, "gear_medium": 0.59, "gear_large": 0.56} + + # Populate event term parameters + self.events.set_robot_to_grasp_pose.params["gear_offsets_grasp"] = self.gear_offsets_grasp + self.events.set_robot_to_grasp_pose.params["end_effector_body_name"] = self.end_effector_body_name + self.events.set_robot_to_grasp_pose.params["num_arm_joints"] = self.num_arm_joints + self.events.set_robot_to_grasp_pose.params["grasp_rot_offset"] = self.grasp_rot_offset + self.events.set_robot_to_grasp_pose.params["gripper_joint_setter_func"] = self.gripper_joint_setter_func + + # Populate termination term parameters + self.terminations.gear_dropped.params["gear_offsets_grasp"] = self.gear_offsets_grasp + self.terminations.gear_dropped.params["end_effector_body_name"] = self.end_effector_body_name + self.terminations.gear_dropped.params["grasp_rot_offset"] = self.grasp_rot_offset + + self.terminations.gear_orientation_exceeded.params["end_effector_body_name"] = self.end_effector_body_name + self.terminations.gear_orientation_exceeded.params["grasp_rot_offset"] = self.grasp_rot_offset + + +@configclass +class UR10e2F85GearAssemblyEnvCfg(UR10eGearAssemblyEnvCfg): + """Configuration for UR10e with Robotiq 2F-85 gripper.""" + + def __post_init__(self): + # post init of parent + super().__post_init__() + + # switch robot to ur10e with 2F-85 gripper + self.scene.robot = UR10e_ROBOTIQ_2F_85_CFG.replace( + prim_path="{ENV_REGEX_NS}/Robot", + spawn=UR10e_ROBOTIQ_2F_85_CFG.spawn.replace( + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=4, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=1 + ), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + # Joint positions based on IK from center of distribution for randomized gear positions + # This is done so that the start for the differential IK search after randomizing + # is close to the optimal grasp pose + init_state=ArticulationCfg.InitialStateCfg( + joint_pos={ + "shoulder_pan_joint": 2.7228, + "shoulder_lift_joint": -8.3962e-01, + "elbow_joint": 1.3684, + "wrist_1_joint": -2.1048, + "wrist_2_joint": -1.5691, + "wrist_3_joint": -1.9896, + }, + pos=(0.0, 0.0, 0.0), + rot=(1.0, 0.0, 0.0, 0.0), + ), + ) + + # 2F-85 gripper actuator configuration (higher effort limits than 2F-140) + self.scene.robot.actuators["gripper_finger"] = ImplicitActuatorCfg( + joint_names_expr=[".*_inner_finger_joint"], + effort_limit_sim=10.0, + velocity_limit_sim=10.0, + stiffness=10.0, + damping=0.05, + friction=0.0, + armature=0.0, + ) + self.scene.robot.actuators["gripper_drive"] = ImplicitActuatorCfg( + joint_names_expr=["finger_joint"], + effort_limit_sim=10.0, + velocity_limit_sim=1.0, + stiffness=40.0, + damping=1.0, + friction=0.0, + armature=0.0, + ) + + # Set gripper-specific joint setter function + self.gripper_joint_setter_func = set_finger_joint_pos_robotiq_2f85 + + # gear offsets and grasp positions for the 2F-85 gripper + self.gear_offsets_grasp = { + "gear_small": [0.0, self.gear_offsets["gear_small"][0], -0.19], + "gear_medium": [0.0, self.gear_offsets["gear_medium"][0], -0.19], + "gear_large": [0.0, self.gear_offsets["gear_large"][0], -0.19], + } + + # Grasp widths for 2F-85 gripper + self.hand_grasp_width = {"gear_small": 0.64, "gear_medium": 0.46, "gear_large": 0.4} + + # Close widths for 2F-85 gripper + self.hand_close_width = {"gear_small": 0.69, "gear_medium": 0.51, "gear_large": 0.45} + + # Populate event term parameters + self.events.set_robot_to_grasp_pose.params["gear_offsets_grasp"] = self.gear_offsets_grasp + self.events.set_robot_to_grasp_pose.params["end_effector_body_name"] = self.end_effector_body_name + self.events.set_robot_to_grasp_pose.params["num_arm_joints"] = self.num_arm_joints + self.events.set_robot_to_grasp_pose.params["grasp_rot_offset"] = self.grasp_rot_offset + self.events.set_robot_to_grasp_pose.params["gripper_joint_setter_func"] = self.gripper_joint_setter_func + + # Populate termination term parameters + self.terminations.gear_dropped.params["gear_offsets_grasp"] = self.gear_offsets_grasp + self.terminations.gear_dropped.params["end_effector_body_name"] = self.end_effector_body_name + self.terminations.gear_dropped.params["grasp_rot_offset"] = self.grasp_rot_offset + + self.terminations.gear_orientation_exceeded.params["end_effector_body_name"] = self.end_effector_body_name + self.terminations.gear_orientation_exceeded.params["grasp_rot_offset"] = self.grasp_rot_offset + + +@configclass +class UR10e2F140GearAssemblyEnvCfg_PLAY(UR10e2F140GearAssemblyEnvCfg): + """Play configuration for UR10e with Robotiq 2F-140 gripper.""" + + def __post_init__(self): + # post init of parent + super().__post_init__() + # make a smaller scene for play + self.scene.num_envs = 50 + self.scene.env_spacing = 2.5 + # disable randomization for play + self.observations.policy.enable_corruption = False + + +@configclass +class UR10e2F85GearAssemblyEnvCfg_PLAY(UR10e2F85GearAssemblyEnvCfg): + """Play configuration for UR10e with Robotiq 2F-85 gripper.""" + + def __post_init__(self): + # post init of parent + super().__post_init__() + # make a smaller scene for play + self.scene.num_envs = 50 + self.scene.env_spacing = 2.5 + # disable randomization for play + self.observations.policy.enable_corruption = False diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/ros_inference_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/ros_inference_env_cfg.py new file mode 100644 index 000000000000..450a454f78f0 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/config/ur_10e/ros_inference_env_cfg.py @@ -0,0 +1,197 @@ +# Copyright (c) 2025-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 math + +from isaaclab.assets import RigidObjectCfg +from isaaclab.utils import configclass + +from .joint_pos_env_cfg import UR10e2F85GearAssemblyEnvCfg, UR10e2F140GearAssemblyEnvCfg + + +@configclass +class UR10e2F140GearAssemblyROSInferenceEnvCfg(UR10e2F140GearAssemblyEnvCfg): + """Configuration for ROS inference with UR10e and Robotiq 2F-140 gripper. + + This configuration: + - Exposes variables needed for ROS inference + - Overrides robot and gear initial poses for fixed/deterministic setup + """ + + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Variables used by Isaac Manipulator for on robot inference + # These parameters allow the ROS inference node to validate environment configuration, + # perform checks during inference, and correctly interpret observations and actions. + self.obs_order = ["arm_dof_pos", "arm_dof_vel", "shaft_pos", "shaft_quat"] + self.policy_action_space = "joint" + # Use inherited joint names from parent's observation configuration + self.arm_joint_names = self.observations.policy.joint_pos.params["asset_cfg"].joint_names + # Use inherited num_arm_joints from parent + self.action_space = self.num_arm_joints + # State space and observation space are set as constants for now + self.state_space = 42 + self.observation_space = 19 + + # Set joint_action_scale from the existing arm_action.scale + self.joint_action_scale = self.actions.arm_action.scale + + # Dynamically generate action_scale_joint_space based on action_space + self.action_scale_joint_space = [self.joint_action_scale] * self.action_space + + # Override robot initial pose for ROS inference (fixed pose, no randomization) + # Note: The policy is trained to work with respect to the UR robot's 'base' frame + # (rotated 180° around Z from base_link), not the base_link frame (USD origin). + # See: https://docs.universal-robots.com/Universal_Robots_ROS2_Documentation/doc/ur_description/doc/robot_frames.html + # Joint positions and pos are inherited from parent, only override rotation to be deterministic + self.scene.robot.init_state.rot = (0.0, 0.0, 0.0, 1.0) + + # Override gear base initial pose (fixed pose for ROS inference) + self.scene.factory_gear_base.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Override gear initial poses (fixed poses for ROS inference) + # Small gear + self.scene.factory_gear_small.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), # z = base_z + 0.1675 (above base) + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Medium gear + self.scene.factory_gear_medium.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Large gear + self.scene.factory_gear_large.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Fixed asset parameters for ROS inference - derived from configuration + # These parameters are used by the ROS inference node to validate the environment setup + # and apply appropriate noise models for robust real-world deployment. + # Derive position center from gear base init state + self.fixed_asset_init_pos_center = list(self.scene.factory_gear_base.init_state.pos) + # Derive position range from parent's randomize_gears_and_base_pose event pose_range + pose_range = self.events.randomize_gears_and_base_pose.params["pose_range"] + self.fixed_asset_init_pos_range = [ + pose_range["x"][1], # max value + pose_range["y"][1], # max value + pose_range["z"][1], # max value + ] + # Orientation in degrees (quaternion (-0.70711, 0.0, 0.0, 0.70711) = -90° around Z) + self.fixed_asset_init_orn_deg = [0.0, 0.0, -90.0] + # Derive orientation range from parent's pose_range (radians to degrees) + self.fixed_asset_init_orn_deg_range = [ + math.degrees(pose_range["roll"][1]), # convert radians to degrees + math.degrees(pose_range["pitch"][1]), + math.degrees(pose_range["yaw"][1]), + ] + # Derive observation noise level from parent's gear_shaft_pos noise configuration + gear_shaft_pos_noise = self.observations.policy.gear_shaft_pos.noise.noise_cfg.n_max + self.fixed_asset_pos_obs_noise_level = [ + gear_shaft_pos_noise, + gear_shaft_pos_noise, + gear_shaft_pos_noise, + ] + + +@configclass +class UR10e2F85GearAssemblyROSInferenceEnvCfg(UR10e2F85GearAssemblyEnvCfg): + """Configuration for ROS inference with UR10e and Robotiq 2F-85 gripper. + + This configuration: + - Exposes variables needed for ROS inference + - Overrides robot and gear initial poses for fixed/deterministic setup + """ + + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Variables used by Isaac Manipulator for on robot inference + # These parameters allow the ROS inference node to validate environment configuration, + # perform checks during inference, and correctly interpret observations and actions. + self.obs_order = ["arm_dof_pos", "arm_dof_vel", "shaft_pos", "shaft_quat"] + self.policy_action_space = "joint" + # Use inherited joint names from parent's observation configuration + self.arm_joint_names = self.observations.policy.joint_pos.params["asset_cfg"].joint_names + # Use inherited num_arm_joints from parent + self.action_space = self.num_arm_joints + # State space and observation space are set as constants for now + self.state_space = 38 + self.observation_space = 19 + + # Set joint_action_scale from the existing arm_action.scale + self.joint_action_scale = self.actions.arm_action.scale + + # Dynamically generate action_scale_joint_space based on action_space + self.action_scale_joint_space = [self.joint_action_scale] * self.action_space + + # Override robot initial pose for ROS inference (fixed pose, no randomization) + # Note: The policy is trained to work with respect to the UR robot's 'base' frame + # (rotated 180° around Z from base_link), not the base_link frame (USD origin). + # See: https://docs.universal-robots.com/Universal_Robots_ROS2_Documentation/doc/ur_description/doc/robot_frames.html + # Joint positions and pos are inherited from parent, only override rotation to be deterministic + self.scene.robot.init_state.rot = (0.0, 0.0, 0.0, 1.0) + + # Override gear base initial pose (fixed pose for ROS inference) + self.scene.factory_gear_base.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Override gear initial poses (fixed poses for ROS inference) + # Small gear + self.scene.factory_gear_small.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), # z = base_z + 0.1675 (above base) + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Medium gear + self.scene.factory_gear_medium.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Large gear + self.scene.factory_gear_large.init_state = RigidObjectCfg.InitialStateCfg( + pos=(1.0200, -0.2100, -0.1), + rot=(-0.70711, 0.0, 0.0, 0.70711), + ) + + # Fixed asset parameters for ROS inference - derived from configuration + # These parameters are used by the ROS inference node to validate the environment setup + # and apply appropriate noise models for robust real-world deployment. + # Derive position center from gear base init state + self.fixed_asset_init_pos_center = list(self.scene.factory_gear_base.init_state.pos) + # Derive position range from parent's randomize_gears_and_base_pose event pose_range + pose_range = self.events.randomize_gears_and_base_pose.params["pose_range"] + self.fixed_asset_init_pos_range = [ + pose_range["x"][1], # max value + pose_range["y"][1], # max value + pose_range["z"][1], # max value + ] + # Orientation in degrees (quaternion (-0.70711, 0.0, 0.0, 0.70711) = -90° around Z) + self.fixed_asset_init_orn_deg = [0.0, 0.0, -90.0] + # Derive orientation range from parent's pose_range (radians to degrees) + self.fixed_asset_init_orn_deg_range = [ + math.degrees(pose_range["roll"][1]), # convert radians to degrees + math.degrees(pose_range["pitch"][1]), + math.degrees(pose_range["yaw"][1]), + ] + # Derive observation noise level from parent's gear_shaft_pos noise configuration + gear_shaft_pos_noise = self.observations.policy.gear_shaft_pos.noise.noise_cfg.n_max + self.fixed_asset_pos_obs_noise_level = [ + gear_shaft_pos_noise, + gear_shaft_pos_noise, + gear_shaft_pos_noise, + ] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/gear_assembly_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/gear_assembly_env_cfg.py new file mode 100644 index 000000000000..8a15d7b3a52d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/gear_assembly/gear_assembly_env_cfg.py @@ -0,0 +1,330 @@ +# Copyright (c) 2025-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 os +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.managers import ActionTermCfg as ActionTerm +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 RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sim.simulation_cfg import PhysxCfg, SimulationCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.noise import UniformNoiseCfg + +import isaaclab_tasks.manager_based.manipulation.deploy.mdp as mdp +import isaaclab_tasks.manager_based.manipulation.deploy.mdp.terminations as gear_assembly_terminations +from isaaclab_tasks.manager_based.manipulation.deploy.mdp.noise_models import ResetSampledConstantNoiseModelCfg + +# Get the directory where this configuration file is located +CONFIG_DIR = os.path.dirname(os.path.abspath(__file__)) +ASSETS_DIR = os.path.join(CONFIG_DIR, "assets") + +## +# Environment configuration +## + + +@configclass +class GearAssemblySceneCfg(InteractiveSceneCfg): + """Configuration for the scene with a robotic arm.""" + + # Disable scene replication to allow USD-level randomization + replicate_physics = False + + # world + ground = AssetBaseCfg( + prim_path="/World/ground", + spawn=sim_utils.GroundPlaneCfg(), + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, -1.05)), + ) + + factory_gear_base = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/FactoryGearBase", + # TODO: change to common isaac sim directory + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Factory/gear_assets/factory_gear_base/factory_gear_base.usd", + activate_contact_sensors=False, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + kinematic_enabled=True, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=32, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=None), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.02, rest_offset=0.0), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-1.0200, 0.2100, -0.1), rot=(0.70711, 0.0, 0.0, 0.70711)), + ) + + factory_gear_small = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/FactoryGearSmall", + # TODO: change to common isaac sim directory + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Factory/gear_assets/factory_gear_small/factory_gear_small.usd", + activate_contact_sensors=False, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + kinematic_enabled=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=32, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=None), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.02, rest_offset=0.0), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-1.0200, 0.2100, -0.1), rot=(0.70711, 0.0, 0.0, 0.70711)), + ) + + factory_gear_medium = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/FactoryGearMedium", + # TODO: change to common isaac sim directory + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Factory/gear_assets/factory_gear_medium/factory_gear_medium.usd", + activate_contact_sensors=False, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + kinematic_enabled=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=32, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=None), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.02, rest_offset=0.0), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-1.0200, 0.2100, -0.1), rot=(0.70711, 0.0, 0.0, 0.70711)), + ) + + factory_gear_large = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/FactoryGearLarge", + # TODO: change to common isaac sim directory + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Factory/gear_assets/factory_gear_large/factory_gear_large.usd", + activate_contact_sensors=False, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + kinematic_enabled=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=32, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=None), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.02, rest_offset=0.0), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-1.0200, 0.2100, -0.1), rot=(0.70711, 0.0, 0.0, 0.70711)), + ) + + # robots + robot: ArticulationCfg = MISSING + + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=2500.0), + ) + + stand = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Stand", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/Stand/stand_instanceable.usd", scale=(2.0, 2.0, 2.0) + ), + ) + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + arm_action: ActionTerm = MISSING + gripper_action: ActionTerm | None = None + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + # observation terms (order preserved) + joint_pos = ObsTerm(func=mdp.joint_pos, params={"asset_cfg": SceneEntityCfg("robot", joint_names=[".*"])}) + joint_vel = ObsTerm(func=mdp.joint_vel, params={"asset_cfg": SceneEntityCfg("robot", joint_names=[".*"])}) + gear_shaft_pos = ObsTerm( + func=mdp.gear_shaft_pos_w, + params={}, # Will be populated in __post_init__ + noise=ResetSampledConstantNoiseModelCfg( + noise_cfg=UniformNoiseCfg(n_min=-0.005, n_max=0.005, operation="add") + ), + ) + gear_shaft_quat = ObsTerm(func=mdp.gear_shaft_quat_w) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + + @configclass + class CriticCfg(ObsGroup): + """Observations for policy group.""" + + # observation terms (order preserved) + joint_pos = ObsTerm(func=mdp.joint_pos, params={"asset_cfg": SceneEntityCfg("robot", joint_names=[".*"])}) + joint_vel = ObsTerm(func=mdp.joint_vel, params={"asset_cfg": SceneEntityCfg("robot", joint_names=[".*"])}) + gear_shaft_pos = ObsTerm(func=mdp.gear_shaft_pos_w, params={}) # Will be populated in __post_init__ + gear_shaft_quat = ObsTerm(func=mdp.gear_shaft_quat_w) + + gear_pos = ObsTerm(func=mdp.gear_pos_w) + gear_quat = ObsTerm(func=mdp.gear_quat_w) + + # observation groups + policy: PolicyCfg = PolicyCfg() + critic: CriticCfg = CriticCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + reset_all = EventTerm(func=mdp.reset_scene_to_default, mode="reset") + + reset_gear = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": { + "x": [-0.05, 0.05], + "y": [-0.05, 0.05], + "z": [0.1, 0.15], + }, + "velocity_range": {}, + "asset_cfg": SceneEntityCfg("factory_gear_small"), + }, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + end_effector_gear_keypoint_tracking = RewTerm( + func=mdp.keypoint_entity_error, + weight=-1.5, + params={ + "asset_cfg_1": SceneEntityCfg("factory_gear_base"), + "keypoint_scale": 0.15, + }, + ) + + end_effector_gear_keypoint_tracking_exp = RewTerm( + func=mdp.keypoint_entity_error_exp, + weight=1.5, + params={ + "asset_cfg_1": SceneEntityCfg("factory_gear_base"), + "kp_exp_coeffs": [(50, 0.0001), (300, 0.0001)], + "kp_use_sum_of_exps": False, + "keypoint_scale": 0.15, + }, + ) + + action_rate = RewTerm(func=mdp.action_rate_l2, weight=-5.0e-06) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + gear_dropped = DoneTerm( + func=gear_assembly_terminations.reset_when_gear_dropped, + params={ + "distance_threshold": 0.15, # 15cm from gripper + "robot_asset_cfg": SceneEntityCfg("robot"), + }, + ) + + gear_orientation_exceeded = DoneTerm( + func=gear_assembly_terminations.reset_when_gear_orientation_exceeds_threshold, + params={ + "roll_threshold_deg": 7.0, # Maximum roll deviation in degrees + "pitch_threshold_deg": 7.0, # Maximum pitch deviation in degrees + "yaw_threshold_deg": 180.0, # Maximum yaw deviation in degrees + "robot_asset_cfg": SceneEntityCfg("robot"), + }, + ) + + +@configclass +class GearAssemblyEnvCfg(ManagerBasedRLEnvCfg): + # Scene settings + scene: GearAssemblySceneCfg = GearAssemblySceneCfg(num_envs=4096, env_spacing=2.5) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + sim: SimulationCfg = SimulationCfg( + physx=PhysxCfg( + # Important to prevent collisionStackSize buffer overflow in contact-rich environments. + gpu_collision_stack_size=2**28, + gpu_max_rigid_contact_count=2**23, + gpu_max_rigid_patch_count=2**23, + ), + ) + + def __post_init__(self): + """Post initialization.""" + # general settings + self.episode_length_s = 6.66 + self.viewer.eye = (3.5, 3.5, 3.5) + # simulation settings + self.decimation = 4 + self.sim.render_interval = self.decimation + self.sim.dt = 1.0 / 120.0 + + self.gear_offsets = { + "gear_small": [0.076125, 0.0, 0.0], + "gear_medium": [0.030375, 0.0, 0.0], + "gear_large": [-0.045375, 0.0, 0.0], + } + + # Populate observation term parameters with gear offsets + self.observations.policy.gear_shaft_pos.params["gear_offsets"] = self.gear_offsets + self.observations.critic.gear_shaft_pos.params["gear_offsets"] = self.gear_offsets diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/__init__.py index 6686f9f52766..10ab3ea7e7fd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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,4 +7,8 @@ from isaaclab.envs.mdp import * # noqa: F401, F403 +from .events import * # noqa: F401, F403 +from .noise_models import * # noqa: F401, F403 +from .observations import * # noqa: F401, F403 from .rewards import * # noqa: F401, F403 +from .terminations import * # noqa: F401, F403 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/events.py new file mode 100644 index 000000000000..7666875435fb --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/events.py @@ -0,0 +1,481 @@ +# Copyright (c) 2025-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 + +"""Class-based event terms specific to the gear assembly manipulation environments.""" + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg + +from isaaclab_tasks.direct.automate import factory_control as fc + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + + +class randomize_gear_type(ManagerTermBase): + """Randomize and manage the gear type being used for each environment. + + This class stores the current gear type for each environment and provides a mapping + from gear type names to indices. It serves as the central manager for gear type state + that other MDP terms depend on. + """ + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + """Initialize the gear type randomization term. + + Args: + cfg: Event term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Extract gear types from config (required parameter) + if "gear_types" not in cfg.params: + raise ValueError("'gear_types' parameter is required in randomize_gear_type configuration") + self.gear_types: list[str] = cfg.params["gear_types"] + + # Create gear type mapping (shared across all terms) + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + + # Store current gear type for each environment (as list for easy access) + # Initialize all to first gear type in the list + self._current_gear_type = [self.gear_types[0]] * env.num_envs + + # Store current gear type indices as tensor for efficient vectorized access + # Initialize all to first gear type index + first_gear_idx = self.gear_type_map[self.gear_types[0]] + self._current_gear_type_indices = torch.full( + (env.num_envs,), first_gear_idx, device=env.device, dtype=torch.long + ) + + # Store reference on environment for other terms to access + env._gear_type_manager = self + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + gear_types: list[str] = ["gear_small", "gear_medium", "gear_large"], + ): + """Randomize the gear type for specified environments. + + Args: + env: The environment containing the assets + env_ids: Environment IDs to randomize + gear_types: List of available gear types to choose from + """ + # Randomly select gear type for each environment + # Use the parameter passed to __call__ (not self.gear_types) to allow runtime overrides + for env_id in env_ids.tolist(): + chosen_gear = random.choice(gear_types) + self._current_gear_type[env_id] = chosen_gear + self._current_gear_type_indices[env_id] = self.gear_type_map[chosen_gear] + + def get_gear_type(self, env_id: int) -> str: + """Get the current gear type for a specific environment.""" + return self._current_gear_type[env_id] + + def get_all_gear_types(self) -> list[str]: + """Get current gear types for all environments.""" + return self._current_gear_type + + def get_all_gear_type_indices(self) -> torch.Tensor: + """Get current gear type indices for all environments as a tensor. + + Returns: + Tensor of shape (num_envs,) with gear type indices (0=small, 1=medium, 2=large) + """ + return self._current_gear_type_indices + + +class set_robot_to_grasp_pose(ManagerTermBase): + """Set robot to grasp pose using IK with pre-cached tensors. + + This class-based term caches all required tensors and gear offsets during initialization, + avoiding repeated allocations and lookups during execution. + """ + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + """Initialize the set robot to grasp pose term. + + Args: + cfg: Event term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Get robot asset configuration + self.robot_asset_cfg: SceneEntityCfg = cfg.params.get("robot_asset_cfg", SceneEntityCfg("robot")) + self.robot_asset: Articulation = env.scene[self.robot_asset_cfg.name] + + # Get robot-specific parameters from environment config (all required) + # Validate required parameters + if "end_effector_body_name" not in cfg.params: + raise ValueError( + "'end_effector_body_name' parameter is required in set_robot_to_grasp_pose configuration. " + "Example: 'wrist_3_link'" + ) + if "num_arm_joints" not in cfg.params: + raise ValueError( + "'num_arm_joints' parameter is required in set_robot_to_grasp_pose configuration. Example: 6 for UR10e" + ) + if "grasp_rot_offset" not in cfg.params: + raise ValueError( + "'grasp_rot_offset' parameter is required in set_robot_to_grasp_pose configuration. " + "It should be a quaternion [w, x, y, z]. Example: [0.0, 0.707, 0.707, 0.0]" + ) + if "gripper_joint_setter_func" not in cfg.params: + raise ValueError( + "'gripper_joint_setter_func' parameter is required in set_robot_to_grasp_pose configuration. " + "It should be a function to set gripper joint positions." + ) + + self.end_effector_body_name = cfg.params["end_effector_body_name"] + self.num_arm_joints = cfg.params["num_arm_joints"] + self.gripper_joint_setter_func = cfg.params["gripper_joint_setter_func"] + + # Pre-cache gear grasp offsets as tensors (required parameter) + if "gear_offsets_grasp" not in cfg.params: + raise ValueError( + "'gear_offsets_grasp' parameter is required in set_robot_to_grasp_pose configuration. " + "It should be a dict with keys 'gear_small', 'gear_medium', 'gear_large' mapping to [x, y, z] offsets." + ) + gear_offsets_grasp = cfg.params["gear_offsets_grasp"] + if not isinstance(gear_offsets_grasp, dict): + raise TypeError( + f"'gear_offsets_grasp' parameter must be a dict, got {type(gear_offsets_grasp).__name__}. " + "It should have keys 'gear_small', 'gear_medium', 'gear_large' mapping to [x, y, z] offsets." + ) + + self.gear_grasp_offset_tensors = {} + for gear_type in ["gear_small", "gear_medium", "gear_large"]: + if gear_type not in gear_offsets_grasp: + raise ValueError( + f"'{gear_type}' offset is required in 'gear_offsets_grasp' parameter. " + f"Found keys: {list(gear_offsets_grasp.keys())}" + ) + self.gear_grasp_offset_tensors[gear_type] = torch.tensor( + gear_offsets_grasp[gear_type], device=env.device, dtype=torch.float32 + ) + + # Stack grasp offset tensors for vectorized indexing (shape: 3, 3) + # Index 0=small, 1=medium, 2=large + self.gear_grasp_offsets_stacked = torch.stack( + [ + self.gear_grasp_offset_tensors["gear_small"], + self.gear_grasp_offset_tensors["gear_medium"], + self.gear_grasp_offset_tensors["gear_large"], + ], + dim=0, + ) + + # Pre-cache grasp rotation offset tensor + grasp_rot_offset = cfg.params["grasp_rot_offset"] + self.grasp_rot_offset_tensor = ( + torch.tensor(grasp_rot_offset, device=env.device, dtype=torch.float32).unsqueeze(0).repeat(env.num_envs, 1) + ) + + # Pre-allocate buffers for batch operations + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + self.local_env_indices = torch.arange(env.num_envs, device=env.device) + self.gear_grasp_offsets_buffer = torch.zeros(env.num_envs, 3, device=env.device, dtype=torch.float32) + + # Cache hand grasp/close widths + self.hand_grasp_width = env.cfg.hand_grasp_width + self.hand_close_width = env.cfg.hand_close_width + + # Find end effector index once + eef_indices, _ = self.robot_asset.find_bodies([self.end_effector_body_name]) + if len(eef_indices) == 0: + raise ValueError(f"End effector body '{self.end_effector_body_name}' not found in robot") + self.eef_idx = eef_indices[0] + + # Find jacobian body index (for fixed-base robots, subtract 1) + self.jacobi_body_idx = self.eef_idx - 1 + + # Find all joints once + all_joints, all_joints_names = self.robot_asset.find_joints([".*"]) + self.all_joints = all_joints + self.finger_joints = all_joints[self.num_arm_joints :] + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + robot_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + pos_threshold: float = 1e-6, + rot_threshold: float = 1e-6, + max_iterations: int = 10, + pos_randomization_range: dict | None = None, + gear_offsets_grasp: dict | None = None, + end_effector_body_name: str | None = None, + num_arm_joints: int | None = None, + grasp_rot_offset: list | None = None, + gripper_joint_setter_func: callable | None = None, + ): + """Set robot to grasp pose using IK. + + Args: + env: Environment instance + env_ids: Environment IDs to reset + robot_asset_cfg: Robot asset configuration (unused, kept for compatibility) + pos_threshold: Position convergence threshold + rot_threshold: Rotation convergence threshold + max_iterations: Maximum IK iterations + pos_randomization_range: Optional position randomization range + """ + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this event term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + + # Slice buffers for current batch size + num_reset_envs = len(env_ids) + gear_type_indices = self.gear_type_indices[:num_reset_envs] + local_env_indices = self.local_env_indices[:num_reset_envs] + gear_grasp_offsets = self.gear_grasp_offsets_buffer[:num_reset_envs] + grasp_rot_offset_tensor = self.grasp_rot_offset_tensor[env_ids] + + # IK loop + for i in range(max_iterations): + # Get current joint state + joint_pos = self.robot_asset.data.joint_pos[env_ids].clone() + joint_vel = self.robot_asset.data.joint_vel[env_ids].clone() + + # Stack all gear positions and quaternions + all_gear_pos = torch.stack( + [ + env.scene["factory_gear_small"].data.root_link_pos_w, + env.scene["factory_gear_medium"].data.root_link_pos_w, + env.scene["factory_gear_large"].data.root_link_pos_w, + ], + dim=1, + )[env_ids] + + all_gear_quat = torch.stack( + [ + env.scene["factory_gear_small"].data.root_link_quat_w, + env.scene["factory_gear_medium"].data.root_link_quat_w, + env.scene["factory_gear_large"].data.root_link_quat_w, + ], + dim=1, + )[env_ids] + + # Get gear type indices directly as tensor + all_gear_type_indices = gear_type_manager.get_all_gear_type_indices() + gear_type_indices[:] = all_gear_type_indices[env_ids] + + # Select gear data using advanced indexing + grasp_object_pos_world = all_gear_pos[local_env_indices, gear_type_indices] + grasp_object_quat = all_gear_quat[local_env_indices, gear_type_indices] + + # Apply rotation offset + grasp_object_quat = math_utils.quat_mul(grasp_object_quat, grasp_rot_offset_tensor) + + # Get grasp offsets (vectorized) + gear_grasp_offsets[:] = self.gear_grasp_offsets_stacked[gear_type_indices] + + # Add position randomization if specified + if pos_randomization_range is not None: + pos_keys = ["x", "y", "z"] + range_list_pos = [pos_randomization_range.get(key, (0.0, 0.0)) for key in pos_keys] + ranges_pos = torch.tensor(range_list_pos, device=env.device) + rand_pos_offsets = math_utils.sample_uniform( + ranges_pos[:, 0], ranges_pos[:, 1], (len(env_ids), 3), device=env.device + ) + gear_grasp_offsets = gear_grasp_offsets + rand_pos_offsets + + # Transform offsets from gear frame to world frame + grasp_object_pos_world = grasp_object_pos_world + math_utils.quat_apply( + grasp_object_quat, gear_grasp_offsets + ) + + # Get end effector pose + eef_pos = self.robot_asset.data.body_pos_w[env_ids, self.eef_idx] + eef_quat = self.robot_asset.data.body_quat_w[env_ids, self.eef_idx] + + # Compute pose error + pos_error, axis_angle_error = fc.get_pose_error( + fingertip_midpoint_pos=eef_pos, + fingertip_midpoint_quat=eef_quat, + ctrl_target_fingertip_midpoint_pos=grasp_object_pos_world, + ctrl_target_fingertip_midpoint_quat=grasp_object_quat, + jacobian_type="geometric", + rot_error_type="axis_angle", + ) + delta_hand_pose = torch.cat((pos_error, axis_angle_error), dim=-1) + + # Check convergence + pos_error_norm = torch.norm(pos_error, dim=-1) + rot_error_norm = torch.norm(axis_angle_error, dim=-1) + + if torch.all(pos_error_norm < pos_threshold) and torch.all(rot_error_norm < rot_threshold): + break + + # Solve IK using jacobian + jacobians = self.robot_asset.root_physx_view.get_jacobians().clone() + jacobian = jacobians[env_ids, self.jacobi_body_idx, :, :] + + delta_dof_pos = fc._get_delta_dof_pos( + delta_pose=delta_hand_pose, + ik_method="dls", + jacobian=jacobian, + device=env.device, + ) + + # Update joint positions + joint_pos = joint_pos + delta_dof_pos + joint_vel = torch.zeros_like(joint_pos) + + # Write to sim + self.robot_asset.set_joint_position_target(joint_pos, env_ids=env_ids) + self.robot_asset.set_joint_velocity_target(joint_vel, env_ids=env_ids) + self.robot_asset.write_joint_state_to_sim(joint_pos, joint_vel, env_ids=env_ids) + + # Set gripper to grasp position + joint_pos = self.robot_asset.data.joint_pos[env_ids].clone() + + # Get gear types for all environments + all_gear_types = gear_type_manager.get_all_gear_types() + for row_idx, env_id in enumerate(env_ids.tolist()): + gear_key = all_gear_types[env_id] + hand_grasp_width = self.hand_grasp_width[gear_key] + self.gripper_joint_setter_func(joint_pos, [row_idx], self.finger_joints, hand_grasp_width) + + self.robot_asset.set_joint_position_target(joint_pos, joint_ids=self.all_joints, env_ids=env_ids) + self.robot_asset.write_joint_state_to_sim(joint_pos, joint_vel, env_ids=env_ids) + + # Set gripper to closed position + for row_idx, env_id in enumerate(env_ids.tolist()): + gear_key = all_gear_types[env_id] + hand_close_width = self.hand_close_width[gear_key] + self.gripper_joint_setter_func(joint_pos, [row_idx], self.finger_joints, hand_close_width) + + self.robot_asset.set_joint_position_target(joint_pos, joint_ids=self.all_joints, env_ids=env_ids) + + +class randomize_gears_and_base_pose(ManagerTermBase): + """Randomize both the gear base pose and individual gear poses. + + This class-based term pre-caches all tensors needed for randomization. + """ + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + """Initialize the randomize gears and base pose term. + + Args: + cfg: Event term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Pre-allocate gear type mapping and indices + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + + # Cache asset names + self.gear_asset_names = ["factory_gear_small", "factory_gear_medium", "factory_gear_large"] + self.base_asset_name = "factory_gear_base" + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + pose_range: dict = {}, + velocity_range: dict = {}, + gear_pos_range: dict = {}, + ): + """Randomize gear base and gear poses. + + Args: + env: Environment instance + env_ids: Environment IDs to randomize + pose_range: Pose randomization range for base and all gears + velocity_range: Velocity randomization range + gear_pos_range: Additional position randomization for selected gear only + """ + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this event term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + device = env.device + + # Shared pose samples for all assets + pose_keys = ["x", "y", "z", "roll", "pitch", "yaw"] + range_list_pose = [pose_range.get(key, (0.0, 0.0)) for key in pose_keys] + ranges_pose = torch.tensor(range_list_pose, device=device) + rand_pose_samples = math_utils.sample_uniform( + ranges_pose[:, 0], ranges_pose[:, 1], (len(env_ids), 6), device=device + ) + + orientations_delta = math_utils.quat_from_euler_xyz( + rand_pose_samples[:, 3], rand_pose_samples[:, 4], rand_pose_samples[:, 5] + ) + + # Shared velocity samples + range_list_vel = [velocity_range.get(key, (0.0, 0.0)) for key in pose_keys] + ranges_vel = torch.tensor(range_list_vel, device=device) + rand_vel_samples = math_utils.sample_uniform( + ranges_vel[:, 0], ranges_vel[:, 1], (len(env_ids), 6), device=device + ) + + # Prepare poses for all assets + positions_by_asset = {} + orientations_by_asset = {} + velocities_by_asset = {} + + asset_names_to_process = [self.base_asset_name] + self.gear_asset_names + for asset_name in asset_names_to_process: + asset: RigidObject | Articulation = env.scene[asset_name] + root_states = asset.data.default_root_state[env_ids].clone() + positions = root_states[:, 0:3] + env.scene.env_origins[env_ids] + rand_pose_samples[:, 0:3] + orientations = math_utils.quat_mul(root_states[:, 3:7], orientations_delta) + velocities = root_states[:, 7:13] + rand_vel_samples + positions_by_asset[asset_name] = positions + orientations_by_asset[asset_name] = orientations + velocities_by_asset[asset_name] = velocities + + # Per-env gear offset (gear_pos_range) applied only to selected gear + range_list_gear = [gear_pos_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]] + ranges_gear = torch.tensor(range_list_gear, device=device) + rand_gear_offsets = math_utils.sample_uniform( + ranges_gear[:, 0], ranges_gear[:, 1], (len(env_ids), 3), device=device + ) + + # Get gear type indices directly as tensor + num_reset_envs = len(env_ids) + gear_type_indices = self.gear_type_indices[:num_reset_envs] + all_gear_type_indices = gear_type_manager.get_all_gear_type_indices() + gear_type_indices[:] = all_gear_type_indices[env_ids] + + # Apply offsets using vectorized operations with masks + for gear_idx, asset_name in enumerate(self.gear_asset_names): + if asset_name in positions_by_asset: + mask = gear_type_indices == gear_idx + positions_by_asset[asset_name][mask] = positions_by_asset[asset_name][mask] + rand_gear_offsets[mask] + + # Write to sim + for asset_name in positions_by_asset.keys(): + asset = env.scene[asset_name] + positions = positions_by_asset[asset_name] + orientations = orientations_by_asset[asset_name] + velocities = velocities_by_asset[asset_name] + asset.write_root_pose_to_sim(torch.cat([positions, orientations], dim=-1), env_ids=env_ids) + asset.write_root_velocity_to_sim(velocities, env_ids=env_ids) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/noise_models.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/noise_models.py new file mode 100644 index 000000000000..2d5411e96977 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/noise_models.py @@ -0,0 +1,109 @@ +# Copyright (c) 2025-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 + +"""Noise models specific to deployment tasks.""" + +from __future__ import annotations + +__all__ = ["ResetSampledConstantNoiseModel", "ResetSampledConstantNoiseModelCfg"] + +from collections.abc import Sequence +from dataclasses import MISSING +from typing import TYPE_CHECKING + +import torch + +from isaaclab.utils import configclass +from isaaclab.utils.noise import NoiseModel, NoiseModelCfg + +if TYPE_CHECKING: + from isaaclab.utils.noise import NoiseCfg + + +class ResetSampledConstantNoiseModel(NoiseModel): + """Noise model that samples noise ONLY during reset and applies it consistently. + + The noise is sampled from the configured distribution ONLY during reset and applied consistently + until the next reset. Unlike regular noise that generates new random values every step, + this model maintains the same noise values throughout an episode. + + Note: + This noise model was used since the noise randimization should only be done at reset time. + Other noise models(Eg: GaussianNoise) were not used since this randomizes the noise at every time-step. + """ + + def __init__(self, noise_model_cfg: NoiseModelCfg, num_envs: int, device: str): + # initialize parent class + super().__init__(noise_model_cfg, num_envs, device) + # store the noise configuration + self._noise_cfg = noise_model_cfg.noise_cfg + self._sampled_noise = torch.zeros((num_envs, 1), device=self._device) + self._num_components: int | None = None + + def reset(self, env_ids: Sequence[int] | None = None): + """Reset the noise model by sampling NEW noise values. + + This method samples new noise for the specified environments using the configured noise function. + The sampled noise will remain constant until the next reset. + + Args: + env_ids: The environment ids to reset the noise model for. Defaults to None, + in which case all environments are considered. + """ + # resolve the environment ids + if env_ids is None: + env_ids = slice(None) + + # Use the existing noise function to sample new noise + # Create dummy data to sample from the noise function + dummy_data = torch.zeros( + (env_ids.stop - env_ids.start if isinstance(env_ids, slice) else len(env_ids), 1), device=self._device + ) + + # Sample noise using the configured noise function + sampled_noise = self._noise_model_cfg.noise_cfg.func(dummy_data, self._noise_model_cfg.noise_cfg) + + self._sampled_noise[env_ids] = sampled_noise + + def __call__(self, data: torch.Tensor) -> torch.Tensor: + """Apply the pre-sampled noise to the data. + + This method applies the noise that was sampled during the last reset. + No new noise is generated - the same values are used consistently. + + Args: + data: The data to apply the noise to. Shape is (num_envs, ...). + + Returns: + The data with the noise applied. Shape is the same as the input data. + """ + # on first apply, expand noise to match last dim of data + if self._num_components is None: + *_, self._num_components = data.shape + # expand noise from (num_envs,1) to (num_envs, num_components) + self._sampled_noise = self._sampled_noise.repeat(1, self._num_components) + + # apply the noise based on operation + if self._noise_cfg.operation == "add": + return data + self._sampled_noise + elif self._noise_cfg.operation == "scale": + return data * self._sampled_noise + elif self._noise_cfg.operation == "abs": + return self._sampled_noise + else: + raise ValueError(f"Unknown operation in noise: {self._noise_cfg.operation}") + + +@configclass +class ResetSampledConstantNoiseModelCfg(NoiseModelCfg): + """Configuration for a noise model that samples noise ONLY during reset.""" + + class_type: type = ResetSampledConstantNoiseModel + + noise_cfg: NoiseCfg = MISSING + """The noise configuration for the noise. + + Based on this configuration, the noise is sampled at every reset of the noise model. + """ diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/observations.py new file mode 100644 index 000000000000..cf9ae56ee2da --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/observations.py @@ -0,0 +1,342 @@ +# 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 + +"""Class-based observation terms for the gear assembly manipulation environment.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +from isaaclab.assets import RigidObject +from isaaclab.managers import ManagerTermBase, ObservationTermCfg, SceneEntityCfg +from isaaclab.utils.math import combine_frame_transforms + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + from .events import randomize_gear_type + + +class gear_shaft_pos_w(ManagerTermBase): + """Gear shaft position in world frame with offset applied. + + This class-based term caches gear offset tensors and identity quaternions for efficient computation + across all environments. It transforms the gear base position by the appropriate offset based on the + active gear type in each environment. + + Args: + asset_cfg: The asset configuration for the gear base. Defaults to SceneEntityCfg("factory_gear_base"). + gear_offsets: A dictionary mapping gear type names to their shaft offsets in the gear base frame. + Required keys are "gear_small", "gear_medium", and "gear_large", each mapping to a 3D offset + list [x, y, z]. This parameter is required and must be provided in the configuration. + + Returns: + Gear shaft position tensor in the environment frame with shape (num_envs, 3). + + Raises: + ValueError: If the 'gear_offsets' parameter is not provided in the configuration. + TypeError: If the 'gear_offsets' parameter is not a dictionary. + ValueError: If any of the required gear type keys are missing from 'gear_offsets'. + RuntimeError: If the gear type manager is not initialized in the environment. + """ + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + """Initialize the gear shaft position observation term. + + Args: + cfg: Observation term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Cache asset + self.asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg", SceneEntityCfg("factory_gear_base")) + self.asset: RigidObject = env.scene[self.asset_cfg.name] + + # Pre-cache gear offset tensors (required parameter) + if "gear_offsets" not in cfg.params: + raise ValueError( + "'gear_offsets' parameter is required in gear_shaft_pos_w configuration. " + "It should be a dict with keys 'gear_small', 'gear_medium', 'gear_large' mapping to [x, y, z] offsets." + ) + gear_offsets = cfg.params["gear_offsets"] + if not isinstance(gear_offsets, dict): + raise TypeError( + f"'gear_offsets' parameter must be a dict, got {type(gear_offsets).__name__}. " + "It should have keys 'gear_small', 'gear_medium', 'gear_large' mapping to [x, y, z] offsets." + ) + + self.gear_offset_tensors = {} + for gear_type in ["gear_small", "gear_medium", "gear_large"]: + if gear_type not in gear_offsets: + raise ValueError( + f"'{gear_type}' offset is required in 'gear_offsets' parameter. " + f"Found keys: {list(gear_offsets.keys())}" + ) + self.gear_offset_tensors[gear_type] = torch.tensor( + gear_offsets[gear_type], device=env.device, dtype=torch.float32 + ) + + # Stack offset tensors for vectorized indexing (shape: 3, 3) + # Index 0=small, 1=medium, 2=large + self.gear_offsets_stacked = torch.stack( + [ + self.gear_offset_tensors["gear_small"], + self.gear_offset_tensors["gear_medium"], + self.gear_offset_tensors["gear_large"], + ], + dim=0, + ) + + # Pre-allocate buffers + self.offsets_buffer = torch.zeros(env.num_envs, 3, device=env.device, dtype=torch.float32) + self.env_indices = torch.arange(env.num_envs, device=env.device) + self.identity_quat = ( + torch.tensor([[1.0, 0.0, 0.0, 0.0]], device=env.device, dtype=torch.float32) + .repeat(env.num_envs, 1) + .contiguous() + ) + + def __call__( + self, + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg = SceneEntityCfg("factory_gear_base"), + gear_offsets: dict | None = None, + ) -> torch.Tensor: + """Compute gear shaft position in world frame. + + Args: + env: Environment instance + asset_cfg: Configuration of the gear base asset (unused, kept for compatibility) + + Returns: + Gear shaft position tensor of shape (num_envs, 3) + """ + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this observation term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + # Get gear type indices directly as tensor (no Python loops!) + gear_type_indices = gear_type_manager.get_all_gear_type_indices() + + # Get base gear position and orientation + base_pos = self.asset.data.root_pos_w + base_quat = self.asset.data.root_quat_w + + # Update offsets using vectorized indexing + self.offsets_buffer = self.gear_offsets_stacked[gear_type_indices] + + # Transform offsets + shaft_pos, _ = combine_frame_transforms(base_pos, base_quat, self.offsets_buffer, self.identity_quat) + + return shaft_pos - env.scene.env_origins + + +class gear_shaft_quat_w(ManagerTermBase): + """Gear shaft orientation in world frame. + + This class-based term returns the orientation of the gear base (which is the same as the gear shaft + orientation). The quaternion is canonicalized to ensure the w component is positive, reducing + observation variation for the policy. + + Args: + asset_cfg: The asset configuration for the gear base. Defaults to SceneEntityCfg("factory_gear_base"). + + Returns: + Gear shaft orientation tensor as a quaternion (w, x, y, z) with shape (num_envs, 4). + """ + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + """Initialize the gear shaft orientation observation term. + + Args: + cfg: Observation term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Cache asset + self.asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg", SceneEntityCfg("factory_gear_base")) + self.asset: RigidObject = env.scene[self.asset_cfg.name] + + def __call__( + self, + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg = SceneEntityCfg("factory_gear_base"), + ) -> torch.Tensor: + """Compute gear shaft orientation in world frame. + + Args: + env: Environment instance + asset_cfg: Configuration of the gear base asset (unused, kept for compatibility) + + Returns: + Gear shaft orientation tensor of shape (num_envs, 4) + """ + # Get base quaternion + base_quat = self.asset.data.root_quat_w + + # Ensure w component is positive (q and -q represent the same rotation) + # Pick one canonical form to reduce observation variation seen by the policy + w_negative = base_quat[:, 0] < 0 + positive_quat = base_quat.clone() + positive_quat[w_negative] = -base_quat[w_negative] + + return positive_quat + + +class gear_pos_w(ManagerTermBase): + """Gear position in world frame. + + This class-based term returns the position of the active gear in each environment. It uses + vectorized indexing to efficiently select the correct gear position based on the gear type + (small, medium, or large) active in each environment. + + Returns: + Gear position tensor in the environment frame with shape (num_envs, 3). + + Raises: + RuntimeError: If the gear type manager is not initialized in the environment. + """ + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + """Initialize the gear position observation term. + + Args: + cfg: Observation term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Pre-allocate gear type mapping and indices + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + self.env_indices = torch.arange(env.num_envs, device=env.device) + + # Cache gear assets + self.gear_assets = { + "gear_small": env.scene["factory_gear_small"], + "gear_medium": env.scene["factory_gear_medium"], + "gear_large": env.scene["factory_gear_large"], + } + + def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor: + """Compute gear position in world frame. + + Args: + env: Environment instance + + Returns: + Gear position tensor of shape (num_envs, 3) + """ + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this observation term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + # Get gear type indices directly as tensor (no Python loops!) + self.gear_type_indices = gear_type_manager.get_all_gear_type_indices() + + # Stack all gear positions + all_gear_positions = torch.stack( + [ + self.gear_assets["gear_small"].data.root_pos_w, + self.gear_assets["gear_medium"].data.root_pos_w, + self.gear_assets["gear_large"].data.root_pos_w, + ], + dim=1, + ) + + # Select gear positions using advanced indexing + gear_positions = all_gear_positions[self.env_indices, self.gear_type_indices] + + return gear_positions - env.scene.env_origins + + +class gear_quat_w(ManagerTermBase): + """Gear orientation in world frame. + + This class-based term returns the orientation of the active gear in each environment. It uses + vectorized indexing to efficiently select the correct gear orientation based on the gear type + (small, medium, or large) active in each environment. The quaternion is canonicalized to ensure + the w component is positive, reducing observation variation for the policy. + + Returns: + Gear orientation tensor as a quaternion (w, x, y, z) with shape (num_envs, 4). + + Raises: + RuntimeError: If the gear type manager is not initialized in the environment. + """ + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + """Initialize the gear orientation observation term. + + Args: + cfg: Observation term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Pre-allocate gear type mapping and indices + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + self.env_indices = torch.arange(env.num_envs, device=env.device) + + # Cache gear assets + self.gear_assets = { + "gear_small": env.scene["factory_gear_small"], + "gear_medium": env.scene["factory_gear_medium"], + "gear_large": env.scene["factory_gear_large"], + } + + def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor: + """Compute gear orientation in world frame. + + Args: + env: Environment instance + + Returns: + Gear orientation tensor of shape (num_envs, 4) + """ + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this observation term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + # Get gear type indices directly as tensor (no Python loops!) + self.gear_type_indices = gear_type_manager.get_all_gear_type_indices() + + # Stack all gear quaternions + all_gear_quat = torch.stack( + [ + self.gear_assets["gear_small"].data.root_quat_w, + self.gear_assets["gear_medium"].data.root_quat_w, + self.gear_assets["gear_large"].data.root_quat_w, + ], + dim=1, + ) + + # Select gear quaternions using advanced indexing + gear_quat = all_gear_quat[self.env_indices, self.gear_type_indices] + + # Ensure w component is positive (q and -q represent the same rotation) + # Pick one canonical form to reduce observation variation seen by the policy + w_negative = gear_quat[:, 0] < 0 + gear_positive_quat = gear_quat.clone() + gear_positive_quat[w_negative] = -gear_quat[w_negative] + + return gear_positive_quat diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/rewards.py index 0d14620e225e..482cab8f69b8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/rewards.py @@ -1,23 +1,407 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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 +"""Class-based reward terms for the gear assembly manipulation environment.""" + from __future__ import annotations -import torch from typing import TYPE_CHECKING -from isaacsim.core.utils.torch.transformations import tf_combine +import torch -from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import ManagerTermBase, RewardTermCfg, SceneEntityCfg from isaaclab.sensors.frame_transformer.frame_transformer import FrameTransformer +from isaaclab.utils.math import combine_frame_transforms if TYPE_CHECKING: from isaaclab.envs import ManagerBasedRLEnv + from .events import randomize_gear_type + + +class keypoint_command_error(ManagerTermBase): + """Compute keypoint distance between current and desired poses from command. + + This class-based term uses _compute_keypoint_distance internally. + """ + + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + """Initialize the keypoint command error term. + + Args: + cfg: Reward term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Cache asset configuration + self.asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg", SceneEntityCfg("ee_frame")) + self.command_name: str = cfg.params.get("command_name", "ee_pose") + + # Create keypoint distance computer + self.keypoint_computer = _compute_keypoint_distance(cfg, env) + + def __call__( + self, + env: ManagerBasedRLEnv, + command_name: str, + asset_cfg: SceneEntityCfg, + keypoint_scale: float = 1.0, + add_cube_center_kp: bool = True, + ) -> torch.Tensor: + """Compute keypoint distance error. + + Args: + env: Environment instance + command_name: Name of the command containing desired pose + asset_cfg: Configuration of the asset to track + keypoint_scale: Scale factor for keypoint offsets + add_cube_center_kp: Whether to include center keypoint + + Returns: + Mean keypoint distance tensor of shape (num_envs,) + """ + # Extract frame transformer sensor + asset: FrameTransformer = env.scene[asset_cfg.name] + command = env.command_manager.get_command(command_name) + + # Get desired pose from command + des_pos_w = command[:, :3] + des_quat_w = command[:, 3:7] + + # Get current pose from frame transformer + curr_pos_w = asset.data.target_pos_source[:, 0] + curr_quat_w = asset.data.target_quat_source[:, 0] + + # Compute keypoint distance + keypoint_dist_sep = self.keypoint_computer.compute( + current_pos=curr_pos_w, + current_quat=curr_quat_w, + target_pos=des_pos_w, + target_quat=des_quat_w, + keypoint_scale=keypoint_scale, + ) + + return keypoint_dist_sep.mean(-1) + + +class keypoint_command_error_exp(ManagerTermBase): + """Compute exponential keypoint reward between current and desired poses from command. -def get_keypoint_offsets_full_6d(add_cube_center_kp: bool = False, device: torch.device | None = None) -> torch.Tensor: + This class-based term uses _compute_keypoint_distance internally and applies + exponential reward transformation. + """ + + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + """Initialize the keypoint command error exponential term. + + Args: + cfg: Reward term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Cache asset configuration + self.asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg", SceneEntityCfg("ee_frame")) + self.command_name: str = cfg.params.get("command_name", "ee_pose") + + # Create keypoint distance computer + self.keypoint_computer = _compute_keypoint_distance(cfg, env) + + def __call__( + self, + env: ManagerBasedRLEnv, + command_name: str, + asset_cfg: SceneEntityCfg, + kp_exp_coeffs: list[tuple[float, float]] = [(1.0, 0.1)], + kp_use_sum_of_exps: bool = True, + keypoint_scale: float = 1.0, + add_cube_center_kp: bool = True, + ) -> torch.Tensor: + """Compute exponential keypoint reward. + + Args: + env: Environment instance + command_name: Name of the command containing desired pose + asset_cfg: Configuration of the asset to track + kp_exp_coeffs: List of (a, b) coefficient pairs for exponential reward + kp_use_sum_of_exps: Whether to use sum of exponentials + keypoint_scale: Scale factor for keypoint offsets + add_cube_center_kp: Whether to include center keypoint + + Returns: + Exponential keypoint reward tensor of shape (num_envs,) + """ + # Extract frame transformer sensor + asset: FrameTransformer = env.scene[asset_cfg.name] + command = env.command_manager.get_command(command_name) + + # Get desired pose from command + des_pos_w = command[:, :3] + des_quat_w = command[:, 3:7] + + # Get current pose from frame transformer + curr_pos_w = asset.data.target_pos_source[:, 0] + curr_quat_w = asset.data.target_quat_source[:, 0] + + # Compute keypoint distance + keypoint_dist_sep = self.keypoint_computer.compute( + current_pos=curr_pos_w, + current_quat=curr_quat_w, + target_pos=des_pos_w, + target_quat=des_quat_w, + keypoint_scale=keypoint_scale, + ) + + # Compute exponential reward + keypoint_reward_exp = torch.zeros_like(keypoint_dist_sep[:, 0]) + + if kp_use_sum_of_exps: + for coeff in kp_exp_coeffs: + a, b = coeff + keypoint_reward_exp += ( + 1.0 / (torch.exp(a * keypoint_dist_sep) + b + torch.exp(-a * keypoint_dist_sep)) + ).mean(-1) + else: + keypoint_dist = keypoint_dist_sep.mean(-1) + for coeff in kp_exp_coeffs: + a, b = coeff + keypoint_reward_exp += 1.0 / (torch.exp(a * keypoint_dist) + b + torch.exp(-a * keypoint_dist)) + + return keypoint_reward_exp + + +class keypoint_entity_error(ManagerTermBase): + """Compute keypoint distance between a RigidObject and the dynamically selected gear. + + This class-based term pre-caches gear type mapping and asset references. + """ + + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + """Initialize the keypoint entity error term. + + Args: + cfg: Reward term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Cache asset configuration + self.asset_cfg_1: SceneEntityCfg = cfg.params.get("asset_cfg_1", SceneEntityCfg("factory_gear_base")) + self.asset_1 = env.scene[self.asset_cfg_1.name] + + # Pre-allocate gear type mapping and indices + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + self.env_indices = torch.arange(env.num_envs, device=env.device) + + # Cache gear assets + self.gear_assets = { + "gear_small": env.scene["factory_gear_small"], + "gear_medium": env.scene["factory_gear_medium"], + "gear_large": env.scene["factory_gear_large"], + } + + # Create keypoint distance computer + self.keypoint_computer = _compute_keypoint_distance(cfg, env) + + def __call__( + self, + env: ManagerBasedRLEnv, + asset_cfg_1: SceneEntityCfg, + keypoint_scale: float = 1.0, + add_cube_center_kp: bool = True, + ) -> torch.Tensor: + """Compute keypoint distance error. + + Args: + env: Environment instance + asset_cfg_1: Configuration of the first asset (RigidObject) + keypoint_scale: Scale factor for keypoint offsets + add_cube_center_kp: Whether to include center keypoint + + Returns: + Mean keypoint distance tensor of shape (num_envs,) + """ + # Get current pose of asset_1 (RigidObject) + curr_pos_1 = self.asset_1.data.body_pos_w[:, 0] + curr_quat_1 = self.asset_1.data.body_quat_w[:, 0] + + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this reward term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + # Get gear type indices directly as tensor + self.gear_type_indices = gear_type_manager.get_all_gear_type_indices() + + # Stack all gear positions and quaternions + all_gear_pos = torch.stack( + [ + self.gear_assets["gear_small"].data.body_pos_w[:, 0], + self.gear_assets["gear_medium"].data.body_pos_w[:, 0], + self.gear_assets["gear_large"].data.body_pos_w[:, 0], + ], + dim=1, + ) + + all_gear_quat = torch.stack( + [ + self.gear_assets["gear_small"].data.body_quat_w[:, 0], + self.gear_assets["gear_medium"].data.body_quat_w[:, 0], + self.gear_assets["gear_large"].data.body_quat_w[:, 0], + ], + dim=1, + ) + + # Select positions and quaternions using advanced indexing + curr_pos_2 = all_gear_pos[self.env_indices, self.gear_type_indices] + curr_quat_2 = all_gear_quat[self.env_indices, self.gear_type_indices] + + # Compute keypoint distance + keypoint_dist_sep = self.keypoint_computer.compute( + current_pos=curr_pos_1, + current_quat=curr_quat_1, + target_pos=curr_pos_2, + target_quat=curr_quat_2, + keypoint_scale=keypoint_scale, + ) + + return keypoint_dist_sep.mean(-1) + + +class keypoint_entity_error_exp(ManagerTermBase): + """Compute exponential keypoint reward between a RigidObject and the dynamically selected gear. + + This class-based term pre-caches gear type mapping and asset references. + """ + + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + """Initialize the keypoint entity error exponential term. + + Args: + cfg: Reward term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Cache asset configuration + self.asset_cfg_1: SceneEntityCfg = cfg.params.get("asset_cfg_1", SceneEntityCfg("factory_gear_base")) + self.asset_1 = env.scene[self.asset_cfg_1.name] + + # Pre-allocate gear type mapping and indices + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + self.env_indices = torch.arange(env.num_envs, device=env.device) + + # Cache gear assets + self.gear_assets = { + "gear_small": env.scene["factory_gear_small"], + "gear_medium": env.scene["factory_gear_medium"], + "gear_large": env.scene["factory_gear_large"], + } + + # Create keypoint distance computer + self.keypoint_computer = _compute_keypoint_distance(cfg, env) + + def __call__( + self, + env: ManagerBasedRLEnv, + asset_cfg_1: SceneEntityCfg, + kp_exp_coeffs: list[tuple[float, float]] = [(1.0, 0.1)], + kp_use_sum_of_exps: bool = True, + keypoint_scale: float = 1.0, + add_cube_center_kp: bool = True, + ) -> torch.Tensor: + """Compute exponential keypoint reward. + + Args: + env: Environment instance + asset_cfg_1: Configuration of the first asset (RigidObject) + kp_exp_coeffs: List of (a, b) coefficient pairs for exponential reward + kp_use_sum_of_exps: Whether to use sum of exponentials + keypoint_scale: Scale factor for keypoint offsets + add_cube_center_kp: Whether to include center keypoint + + Returns: + Exponential keypoint reward tensor of shape (num_envs,) + """ + # Get current pose of asset_1 (RigidObject) + curr_pos_1 = self.asset_1.data.body_pos_w[:, 0] + curr_quat_1 = self.asset_1.data.body_quat_w[:, 0] + + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this reward term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + # Get gear type indices directly as tensor + self.gear_type_indices = gear_type_manager.get_all_gear_type_indices() + + # Stack all gear positions and quaternions + all_gear_pos = torch.stack( + [ + self.gear_assets["gear_small"].data.body_pos_w[:, 0], + self.gear_assets["gear_medium"].data.body_pos_w[:, 0], + self.gear_assets["gear_large"].data.body_pos_w[:, 0], + ], + dim=1, + ) + + all_gear_quat = torch.stack( + [ + self.gear_assets["gear_small"].data.body_quat_w[:, 0], + self.gear_assets["gear_medium"].data.body_quat_w[:, 0], + self.gear_assets["gear_large"].data.body_quat_w[:, 0], + ], + dim=1, + ) + + # Select positions and quaternions using advanced indexing + curr_pos_2 = all_gear_pos[self.env_indices, self.gear_type_indices] + curr_quat_2 = all_gear_quat[self.env_indices, self.gear_type_indices] + + # Compute keypoint distance + keypoint_dist_sep = self.keypoint_computer.compute( + current_pos=curr_pos_1, + current_quat=curr_quat_1, + target_pos=curr_pos_2, + target_quat=curr_quat_2, + keypoint_scale=keypoint_scale, + ) + + # Compute exponential reward + keypoint_reward_exp = torch.zeros_like(keypoint_dist_sep[:, 0]) + + if kp_use_sum_of_exps: + for coeff in kp_exp_coeffs: + a, b = coeff + keypoint_reward_exp += ( + 1.0 / (torch.exp(a * keypoint_dist_sep) + b + torch.exp(-a * keypoint_dist_sep)) + ).mean(-1) + else: + keypoint_dist = keypoint_dist_sep.mean(-1) + for coeff in kp_exp_coeffs: + a, b = coeff + keypoint_reward_exp += 1.0 / (torch.exp(a * keypoint_dist) + b + torch.exp(-a * keypoint_dist)) + + return keypoint_reward_exp + + +## +# Helper functions and classes +## + + +def _get_keypoint_offsets_full_6d(add_cube_center_kp: bool = False, device: torch.device | None = None) -> torch.Tensor: """Get keypoints for pose alignment comparison. Pose is aligned if all axis are aligned. Args: @@ -33,199 +417,97 @@ def get_keypoint_offsets_full_6d(add_cube_center_kp: bool = False, device: torch keypoint_corners = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] keypoint_corners = torch.tensor(keypoint_corners, device=device, dtype=torch.float32) - keypoint_corners = torch.cat((keypoint_corners, -keypoint_corners[-3:]), dim=0) # use both negative and positive + keypoint_corners = torch.cat((keypoint_corners, -keypoint_corners[-3:]), dim=0) return keypoint_corners -def compute_keypoint_distance( - current_pos: torch.Tensor, - current_quat: torch.Tensor, - target_pos: torch.Tensor, - target_quat: torch.Tensor, - keypoint_scale: float = 1.0, - add_cube_center_kp: bool = True, - device: torch.device | None = None, -) -> torch.Tensor: +class _compute_keypoint_distance: """Compute keypoint distance between current and target poses. - This function creates keypoints from the current and target poses and calculates - the L2 norm distance between corresponding keypoints. The keypoints are created - by applying offsets to the poses and transforming them to world coordinates. - - Args: - current_pos: Current position tensor of shape (num_envs, 3) - current_quat: Current quaternion tensor of shape (num_envs, 4) - target_pos: Target position tensor of shape (num_envs, 3) - target_quat: Target quaternion tensor of shape (num_envs, 4) - keypoint_scale: Scale factor for keypoint offsets - add_cube_center_kp: Whether to include the center keypoint (0, 0, 0) - device: Device to create tensors on - - Returns: - Keypoint distance tensor of shape (num_envs, num_keypoints) where each element - is the L2 norm distance between corresponding keypoints - """ - if device is None: - device = current_pos.device - - num_envs = current_pos.shape[0] - - # Get keypoint offsets - keypoint_offsets = get_keypoint_offsets_full_6d(add_cube_center_kp, device) - keypoint_offsets = keypoint_offsets * keypoint_scale - num_keypoints = keypoint_offsets.shape[0] - - # Create identity quaternion for transformations - identity_quat = torch.tensor([1.0, 0.0, 0.0, 0.0], device=device).unsqueeze(0).repeat(num_envs, 1) - - # Initialize keypoint tensors - keypoints_current = torch.zeros((num_envs, num_keypoints, 3), device=device) - keypoints_target = torch.zeros((num_envs, num_keypoints, 3), device=device) - - # Compute keypoints for current and target poses - for idx, keypoint_offset in enumerate(keypoint_offsets): - # Transform keypoint offset to world coordinates for current pose - keypoints_current[:, idx] = tf_combine( - current_quat, current_pos, identity_quat, keypoint_offset.repeat(num_envs, 1) - )[1] - - # Transform keypoint offset to world coordinates for target pose - keypoints_target[:, idx] = tf_combine( - target_quat, target_pos, identity_quat, keypoint_offset.repeat(num_envs, 1) - )[1] - # Calculate L2 norm distance between corresponding keypoints - keypoint_dist_sep = torch.norm(keypoints_target - keypoints_current, p=2, dim=-1) - - return keypoint_dist_sep - - -def keypoint_command_error( - env: ManagerBasedRLEnv, - command_name: str, - asset_cfg: SceneEntityCfg, - keypoint_scale: float = 1.0, - add_cube_center_kp: bool = True, -) -> torch.Tensor: - """Compute keypoint distance between current and desired poses from command. - - The function computes the keypoint distance between the current pose of the end effector from - the frame transformer sensor and the desired pose from the command. Keypoints are created by - applying offsets to both poses and the distance is computed as the L2-norm between corresponding keypoints. - - Args: - env: The environment containing the asset - command_name: Name of the command containing desired pose - asset_cfg: Configuration of the asset to track (not used, kept for compatibility) - keypoint_scale: Scale factor for keypoint offsets - add_cube_center_kp: Whether to include the center keypoint (0, 0, 0) - - Returns: - Keypoint distance tensor of shape (num_envs, num_keypoints) where each element - is the L2 norm distance between corresponding keypoints + This helper class pre-caches keypoint offsets and identity quaternions + to avoid repeated allocations during reward computation. """ - # extract the frame transformer sensor - asset: FrameTransformer = env.scene[asset_cfg.name] - command = env.command_manager.get_command(command_name) - - # obtain the desired pose from command (position and orientation) - des_pos_b = command[:, :3] - des_quat_b = command[:, 3:7] - - # transform desired pose to world frame using source frame from frame transformer - des_pos_w = des_pos_b - des_quat_w = des_quat_b - - # get current pose in world frame from frame transformer (end effector pose) - curr_pos_w = asset.data.target_pos_source[:, 0] # First target frame is end_effector - curr_quat_w = asset.data.target_quat_source[:, 0] # First target frame is end_effector - - # compute keypoint distance - keypoint_dist_sep = compute_keypoint_distance( - current_pos=curr_pos_w, - current_quat=curr_quat_w, - target_pos=des_pos_w, - target_quat=des_quat_w, - keypoint_scale=keypoint_scale, - add_cube_center_kp=add_cube_center_kp, - device=curr_pos_w.device, - ) - - # Return mean distance across keypoints to match expected reward shape (num_envs,) - return keypoint_dist_sep.mean(-1) - - -def keypoint_command_error_exp( - env: ManagerBasedRLEnv, - command_name: str, - asset_cfg: SceneEntityCfg, - kp_exp_coeffs: list[tuple[float, float]] = [(1.0, 0.1)], - kp_use_sum_of_exps: bool = True, - keypoint_scale: float = 1.0, - add_cube_center_kp: bool = True, -) -> torch.Tensor: - """Compute exponential keypoint reward between current and desired poses from command. - - The function computes the keypoint distance between the current pose of the end effector from - the frame transformer sensor and the desired pose from the command, then applies an exponential - reward function. The reward is computed using the formula: 1 / (exp(a * distance) + b + exp(-a * distance)) - where a and b are coefficients. - - Args: - env: The environment containing the asset - command_name: Name of the command containing desired pose - asset_cfg: Configuration of the asset to track (not used, kept for compatibility) - kp_exp_coeffs: List of (a, b) coefficient pairs for exponential reward - kp_use_sum_of_exps: Whether to use sum of exponentials (True) or single exponential (False) - keypoint_scale: Scale factor for keypoint offsets - add_cube_center_kp: Whether to include the center keypoint (0, 0, 0) - - Returns: - Exponential keypoint reward tensor of shape (num_envs,) where each element - is the exponential reward value - """ - # extract the frame transformer sensor - asset: FrameTransformer = env.scene[asset_cfg.name] - command = env.command_manager.get_command(command_name) - - # obtain the desired pose from command (position and orientation) - des_pos_b = command[:, :3] - des_quat_b = command[:, 3:7] - - # transform desired pose to world frame using source frame from frame transformer - des_pos_w = des_pos_b - des_quat_w = des_quat_b - - # get current pose in world frame from frame transformer (end effector pose) - curr_pos_w = asset.data.target_pos_source[:, 0] # First target frame is end_effector - curr_quat_w = asset.data.target_quat_source[:, 0] # First target frame is end_effector - - # compute keypoint distance - keypoint_dist_sep = compute_keypoint_distance( - current_pos=curr_pos_w, - current_quat=curr_quat_w, - target_pos=des_pos_w, - target_quat=des_quat_w, - keypoint_scale=keypoint_scale, - add_cube_center_kp=add_cube_center_kp, - device=curr_pos_w.device, - ) - - # compute exponential reward - keypoint_reward_exp = torch.zeros_like(keypoint_dist_sep[:, 0]) # shape: (num_envs,) - - if kp_use_sum_of_exps: - # Use sum of exponentials: average across keypoints for each coefficient - for coeff in kp_exp_coeffs: - a, b = coeff - keypoint_reward_exp += ( - 1.0 / (torch.exp(a * keypoint_dist_sep) + b + torch.exp(-a * keypoint_dist_sep)) - ).mean(-1) - else: - # Use single exponential: average keypoint distance first, then apply exponential - keypoint_dist = keypoint_dist_sep.mean(-1) # shape: (num_envs,) - for coeff in kp_exp_coeffs: - a, b = coeff - keypoint_reward_exp += 1.0 / (torch.exp(a * keypoint_dist) + b + torch.exp(-a * keypoint_dist)) - return keypoint_reward_exp + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + """Initialize the compute keypoint distance helper. + + Args: + cfg: Reward term configuration + env: Environment instance + """ + # Get keypoint configuration + add_cube_center_kp = cfg.params.get("add_cube_center_kp", True) + + # Pre-compute base keypoint offsets (unscaled) + self.keypoint_offsets_base = _get_keypoint_offsets_full_6d( + add_cube_center_kp=add_cube_center_kp, device=env.device + ) + self.num_keypoints = self.keypoint_offsets_base.shape[0] + + # Pre-allocate identity quaternion for keypoint transforms + self.identity_quat_keypoints = ( + torch.tensor([[1.0, 0.0, 0.0, 0.0]], device=env.device, dtype=torch.float32) + .repeat(env.num_envs * self.num_keypoints, 1) + .contiguous() + ) + + # Pre-allocate buffer for batched keypoint offsets + self.keypoint_offsets_buffer = torch.zeros( + env.num_envs, self.num_keypoints, 3, device=env.device, dtype=torch.float32 + ) + + def compute( + self, + current_pos: torch.Tensor, + current_quat: torch.Tensor, + target_pos: torch.Tensor, + target_quat: torch.Tensor, + keypoint_scale: float = 1.0, + ) -> torch.Tensor: + """Compute keypoint distance between current and target poses. + + Args: + current_pos: Current position tensor of shape (num_envs, 3) + current_quat: Current quaternion tensor of shape (num_envs, 4) + target_pos: Target position tensor of shape (num_envs, 3) + target_quat: Target quaternion tensor of shape (num_envs, 4) + keypoint_scale: Scale factor for keypoint offsets + + Returns: + Keypoint distance tensor of shape (num_envs, num_keypoints) + """ + num_envs = current_pos.shape[0] + + # Scale keypoint offsets + keypoint_offsets = self.keypoint_offsets_base * keypoint_scale + + # Create batched keypoints (in-place operation) + self.keypoint_offsets_buffer[:num_envs] = keypoint_offsets.unsqueeze(0) + + # Flatten for batch processing + keypoint_offsets_flat = self.keypoint_offsets_buffer[:num_envs].reshape(-1, 3) + identity_quat = self.identity_quat_keypoints[: num_envs * self.num_keypoints] + + # Expand quaternions and positions for all keypoints + current_quat_expanded = current_quat.unsqueeze(1).expand(-1, self.num_keypoints, -1).reshape(-1, 4) + current_pos_expanded = current_pos.unsqueeze(1).expand(-1, self.num_keypoints, -1).reshape(-1, 3) + target_quat_expanded = target_quat.unsqueeze(1).expand(-1, self.num_keypoints, -1).reshape(-1, 4) + target_pos_expanded = target_pos.unsqueeze(1).expand(-1, self.num_keypoints, -1).reshape(-1, 3) + + # Transform all keypoints at once + keypoints_current_flat, _ = combine_frame_transforms( + current_pos_expanded, current_quat_expanded, keypoint_offsets_flat, identity_quat + ) + keypoints_target_flat, _ = combine_frame_transforms( + target_pos_expanded, target_quat_expanded, keypoint_offsets_flat, identity_quat + ) + + # Reshape back + keypoints_current = keypoints_current_flat.reshape(num_envs, self.num_keypoints, 3) + keypoints_target = keypoints_target_flat.reshape(num_envs, self.num_keypoints, 3) + + # Calculate L2 norm distance + keypoint_dist_sep = torch.norm(keypoints_target - keypoints_current, p=2, dim=-1) + + return keypoint_dist_sep diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/terminations.py new file mode 100644 index 000000000000..b623148c5b3b --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/mdp/terminations.py @@ -0,0 +1,331 @@ +# Copyright (c) 2025-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 + +"""Class-based termination terms specific to the gear assembly manipulation environments.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +import carb + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation +from isaaclab.managers import ManagerTermBase, SceneEntityCfg, TerminationTermCfg + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + + from .events import randomize_gear_type + + +class reset_when_gear_dropped(ManagerTermBase): + """Check if the gear has fallen out of the gripper and return reset flags. + + This class-based term pre-caches all required tensors and gear offsets. + """ + + def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): + """Initialize the reset when gear dropped term. + + Args: + cfg: Termination term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Get robot asset configuration + self.robot_asset_cfg: SceneEntityCfg = cfg.params.get("robot_asset_cfg", SceneEntityCfg("robot")) + self.robot_asset: Articulation = env.scene[self.robot_asset_cfg.name] + + # Validate required parameters + if "end_effector_body_name" not in cfg.params: + raise ValueError( + "'end_effector_body_name' parameter is required in reset_when_gear_dropped configuration. " + "Example: 'wrist_3_link'" + ) + if "grasp_rot_offset" not in cfg.params: + raise ValueError( + "'grasp_rot_offset' parameter is required in reset_when_gear_dropped configuration. " + "It should be a quaternion [w, x, y, z]. Example: [0.0, 0.707, 0.707, 0.0]" + ) + + self.end_effector_body_name = cfg.params["end_effector_body_name"] + + # Pre-cache gear grasp offsets as tensors (required parameter) + if "gear_offsets_grasp" not in cfg.params: + raise ValueError( + "'gear_offsets_grasp' parameter is required in reset_when_gear_dropped configuration. " + "It should be a dict with keys 'gear_small', 'gear_medium', 'gear_large' mapping to [x, y, z] offsets." + ) + gear_offsets_grasp = cfg.params["gear_offsets_grasp"] + if not isinstance(gear_offsets_grasp, dict): + raise TypeError( + f"'gear_offsets_grasp' parameter must be a dict, got {type(gear_offsets_grasp).__name__}. " + "It should have keys 'gear_small', 'gear_medium', 'gear_large' mapping to [x, y, z] offsets." + ) + + self.gear_grasp_offset_tensors = {} + for gear_type in ["gear_small", "gear_medium", "gear_large"]: + if gear_type not in gear_offsets_grasp: + raise ValueError( + f"'{gear_type}' offset is required in 'gear_offsets_grasp' parameter. " + f"Found keys: {list(gear_offsets_grasp.keys())}" + ) + self.gear_grasp_offset_tensors[gear_type] = torch.tensor( + gear_offsets_grasp[gear_type], device=env.device, dtype=torch.float32 + ) + + # Stack grasp offset tensors for vectorized indexing (shape: 3, 3) + # Index 0=small, 1=medium, 2=large + self.gear_grasp_offsets_stacked = torch.stack( + [ + self.gear_grasp_offset_tensors["gear_small"], + self.gear_grasp_offset_tensors["gear_medium"], + self.gear_grasp_offset_tensors["gear_large"], + ], + dim=0, + ) + + # Pre-cache grasp rotation offset tensor + grasp_rot_offset = cfg.params["grasp_rot_offset"] + self.grasp_rot_offset_tensor = ( + torch.tensor(grasp_rot_offset, device=env.device, dtype=torch.float32).unsqueeze(0).repeat(env.num_envs, 1) + ) + + # Pre-allocate buffers + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + self.env_indices = torch.arange(env.num_envs, device=env.device) + self.gear_grasp_offsets_buffer = torch.zeros(env.num_envs, 3, device=env.device, dtype=torch.float32) + self.all_gear_pos_buffer = torch.zeros(env.num_envs, 3, 3, device=env.device, dtype=torch.float32) + self.all_gear_quat_buffer = torch.zeros(env.num_envs, 3, 4, device=env.device, dtype=torch.float32) + self.reset_flags = torch.zeros(env.num_envs, dtype=torch.bool, device=env.device) + + # Cache gear assets + self.gear_assets = { + "gear_small": env.scene["factory_gear_small"], + "gear_medium": env.scene["factory_gear_medium"], + "gear_large": env.scene["factory_gear_large"], + } + + # Find end effector index once + eef_indices, _ = self.robot_asset.find_bodies([self.end_effector_body_name]) + if len(eef_indices) == 0: + carb.log_warn( + f"{self.end_effector_body_name} not found in robot body names. Cannot check gear drop condition." + ) + self.eef_idx = None + else: + self.eef_idx = eef_indices[0] + + def __call__( + self, + env: ManagerBasedEnv, + distance_threshold: float = 0.1, + robot_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + gear_offsets_grasp: dict | None = None, + end_effector_body_name: str | None = None, + grasp_rot_offset: list | None = None, + ) -> torch.Tensor: + """Check if gear has dropped and return reset flags. + + Args: + env: Environment instance + distance_threshold: Maximum allowed distance between gear grasp point and gripper + robot_asset_cfg: Configuration for the robot asset (unused, kept for compatibility) + + Returns: + Boolean tensor indicating which environments should be reset + """ + # Reset flags + self.reset_flags.fill_(False) + + if self.eef_idx is None: + return self.reset_flags + + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this termination term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + # Get gear type indices directly as tensor (no Python loops!) + self.gear_type_indices = gear_type_manager.get_all_gear_type_indices() + + # Get end effector position + eef_pos_world = self.robot_asset.data.body_link_pos_w[:, self.eef_idx] + + # Update gear positions and quaternions in buffers + self.all_gear_pos_buffer[:, 0, :] = self.gear_assets["gear_small"].data.root_link_pos_w + self.all_gear_pos_buffer[:, 1, :] = self.gear_assets["gear_medium"].data.root_link_pos_w + self.all_gear_pos_buffer[:, 2, :] = self.gear_assets["gear_large"].data.root_link_pos_w + + self.all_gear_quat_buffer[:, 0, :] = self.gear_assets["gear_small"].data.root_link_quat_w + self.all_gear_quat_buffer[:, 1, :] = self.gear_assets["gear_medium"].data.root_link_quat_w + self.all_gear_quat_buffer[:, 2, :] = self.gear_assets["gear_large"].data.root_link_quat_w + + # Select gear data using advanced indexing + gear_pos_world = self.all_gear_pos_buffer[self.env_indices, self.gear_type_indices] + gear_quat_world = self.all_gear_quat_buffer[self.env_indices, self.gear_type_indices] + + # Apply rotation offset + gear_quat_world = math_utils.quat_mul(gear_quat_world, self.grasp_rot_offset_tensor) + + # Get grasp offsets (vectorized) + self.gear_grasp_offsets_buffer = self.gear_grasp_offsets_stacked[self.gear_type_indices] + + # Transform grasp offsets to world frame + gear_grasp_pos_world = gear_pos_world + math_utils.quat_apply(gear_quat_world, self.gear_grasp_offsets_buffer) + + # Compute distances + distances = torch.norm(gear_grasp_pos_world - eef_pos_world, dim=-1) + + # Check distance threshold + self.reset_flags[:] = distances > distance_threshold + + return self.reset_flags + + +class reset_when_gear_orientation_exceeds_threshold(ManagerTermBase): + """Check if the gear's orientation relative to the gripper exceeds thresholds. + + This class-based term pre-caches all required tensors and thresholds. + """ + + def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): + """Initialize the reset when gear orientation exceeds threshold term. + + Args: + cfg: Termination term configuration + env: Environment instance + """ + super().__init__(cfg, env) + + # Get robot asset configuration + self.robot_asset_cfg: SceneEntityCfg = cfg.params.get("robot_asset_cfg", SceneEntityCfg("robot")) + self.robot_asset: Articulation = env.scene[self.robot_asset_cfg.name] + + # Validate required parameters + if "end_effector_body_name" not in cfg.params: + raise ValueError( + "'end_effector_body_name' parameter is required in reset_when_gear_orientation_exceeds_threshold" + " configuration. Example: 'wrist_3_link'" + ) + if "grasp_rot_offset" not in cfg.params: + raise ValueError( + "'grasp_rot_offset' parameter is required in reset_when_gear_orientation_exceeds_threshold" + " configuration. It should be a quaternion [w, x, y, z]. Example: [0.0, 0.707, 0.707, 0.0]" + ) + + self.end_effector_body_name = cfg.params["end_effector_body_name"] + + # Pre-cache grasp rotation offset tensor + grasp_rot_offset = cfg.params["grasp_rot_offset"] + self.grasp_rot_offset_tensor = ( + torch.tensor(grasp_rot_offset, device=env.device, dtype=torch.float32).unsqueeze(0).repeat(env.num_envs, 1) + ) + + # Pre-allocate buffers + self.gear_type_map = {"gear_small": 0, "gear_medium": 1, "gear_large": 2} + self.gear_type_indices = torch.zeros(env.num_envs, device=env.device, dtype=torch.long) + self.env_indices = torch.arange(env.num_envs, device=env.device) + self.all_gear_quat_buffer = torch.zeros(env.num_envs, 3, 4, device=env.device, dtype=torch.float32) + self.reset_flags = torch.zeros(env.num_envs, dtype=torch.bool, device=env.device) + + # Cache gear assets + self.gear_assets = { + "gear_small": env.scene["factory_gear_small"], + "gear_medium": env.scene["factory_gear_medium"], + "gear_large": env.scene["factory_gear_large"], + } + + # Find end effector index once + eef_indices, _ = self.robot_asset.find_bodies([self.end_effector_body_name]) + if len(eef_indices) == 0: + carb.log_warn( + f"{self.end_effector_body_name} not found in robot body names. Cannot check gear orientation condition." + ) + self.eef_idx = None + else: + self.eef_idx = eef_indices[0] + + def __call__( + self, + env: ManagerBasedEnv, + roll_threshold_deg: float = 30.0, + pitch_threshold_deg: float = 30.0, + yaw_threshold_deg: float = 180.0, + robot_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + end_effector_body_name: str | None = None, + grasp_rot_offset: list | None = None, + ) -> torch.Tensor: + """Check if gear orientation exceeds thresholds and return reset flags. + + Args: + env: Environment instance + roll_threshold_deg: Maximum allowed roll angle deviation in degrees + pitch_threshold_deg: Maximum allowed pitch angle deviation in degrees + yaw_threshold_deg: Maximum allowed yaw angle deviation in degrees + robot_asset_cfg: Configuration for the robot asset (unused, kept for compatibility) + + Returns: + Boolean tensor indicating which environments should be reset + """ + # Reset flags + self.reset_flags.fill_(False) + + if self.eef_idx is None: + return self.reset_flags + + # Check if gear type manager exists + if not hasattr(env, "_gear_type_manager"): + raise RuntimeError( + "Gear type manager not initialized. Ensure randomize_gear_type event is configured " + "in your environment's event configuration before this termination term is used." + ) + + gear_type_manager: randomize_gear_type = env._gear_type_manager + # Get gear type indices directly as tensor (no Python loops!) + self.gear_type_indices = gear_type_manager.get_all_gear_type_indices() + + # Convert thresholds to radians + roll_threshold_rad = torch.deg2rad(torch.tensor(roll_threshold_deg, device=env.device)) + pitch_threshold_rad = torch.deg2rad(torch.tensor(pitch_threshold_deg, device=env.device)) + yaw_threshold_rad = torch.deg2rad(torch.tensor(yaw_threshold_deg, device=env.device)) + + # Get end effector orientation + eef_quat_world = self.robot_asset.data.body_link_quat_w[:, self.eef_idx] + + # Update gear quaternions in buffer + self.all_gear_quat_buffer[:, 0, :] = self.gear_assets["gear_small"].data.root_link_quat_w + self.all_gear_quat_buffer[:, 1, :] = self.gear_assets["gear_medium"].data.root_link_quat_w + self.all_gear_quat_buffer[:, 2, :] = self.gear_assets["gear_large"].data.root_link_quat_w + + # Select gear data using advanced indexing + gear_quat_world = self.all_gear_quat_buffer[self.env_indices, self.gear_type_indices] + + # Apply rotation offset + gear_quat_world = math_utils.quat_mul(gear_quat_world, self.grasp_rot_offset_tensor) + + # Compute relative orientation: q_rel = q_gear * q_eef^-1 + eef_quat_inv = math_utils.quat_conjugate(eef_quat_world) + relative_quat = math_utils.quat_mul(gear_quat_world, eef_quat_inv) + + # Convert relative quaternion to Euler angles + roll, pitch, yaw = math_utils.euler_xyz_from_quat(relative_quat) + + # Check if any angle exceeds its threshold + self.reset_flags[:] = ( + (torch.abs(roll) > roll_threshold_rad) + | (torch.abs(pitch) > pitch_threshold_rad) + | (torch.abs(yaw) > yaw_threshold_rad) + ) + + return self.reset_flags diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/__init__.py index 09b49256388b..69fa0010cd00 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/__init__.py index 2f9df802cd45..05f9849b508a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/__init__.py index 11548d2c3732..b1626c782186 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/__init__.py index bcc238c84a98..cf59b16a1e2e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/rsl_rl_ppo_cfg.py index 40fe884ed007..02af65eec9a0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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,6 +15,7 @@ class URReachPPORunnerCfg(RslRlOnPolicyRunnerCfg): save_interval = 50 experiment_name = "reach_ur10e" empirical_normalization = True + obs_groups = {"policy": ["policy"], "critic": ["policy"]} policy = RslRlPpoActorCriticCfg( init_noise_std=1.0, actor_hidden_dims=[256, 128, 64], diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/joint_pos_env_cfg.py index e21bc6a7f9f0..4abcf4369764 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/ros_inference_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/ros_inference_env_cfg.py index 4a57028c9808..2324b32b001c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/ros_inference_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/config/ur_10e/ros_inference_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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,7 +30,7 @@ def __post_init__(self): ] self.policy_action_space = "joint" self.action_space = 6 - self.state_space = 0 + self.state_space = 19 self.observation_space = 19 # Set joint_action_scale from the existing arm_action.scale diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/reach_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/reach_env_cfg.py index 767de2160e5e..90b65a0f96c2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/reach_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/deploy/reach/reach_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py index 26075d4da25a..cb7aaceec9c2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py index 52fef8b494ab..de3aca917f75 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py index 4240e6044284..d6250c019573 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py index 159ab6727fb4..8c9e9617fce3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml index f4ac23fcd7a8..3a9e96eaeb05 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py index f7965575737c..9bc92bd8f69b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py index 6c41414f30bd..6b7f82fde06e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py @@ -1,16 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -from isaaclab_assets.robots import KUKA_ALLEGRO_CFG - from isaaclab.managers import ObservationTermCfg as ObsTerm from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import ContactSensorCfg from isaaclab.utils import configclass +from isaaclab_assets.robots import KUKA_ALLEGRO_CFG + from ... import dexsuite_env_cfg as dexsuite from ... import mdp @@ -22,7 +22,6 @@ class KukaAllegroRelJointPosActionCfg: @configclass class KukaAllegroReorientRewardCfg(dexsuite.RewardsCfg): - # bool awarding term if 2 finger tips are in contact with object, one of the contacting fingers has to be thumb. good_finger_contact = RewTerm( func=mdp.contacts, diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py index 75e40c5c74b3..9ee00105e57a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -159,6 +159,7 @@ def __post_init__(self): @configclass class PerceptionObsCfg(ObsGroup): + """Observations for perception group.""" object_point_cloud = ObsTerm( func=mdp.object_point_cloud_b, diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py index 794113f9253f..a6537b1a5e19 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py index a51325581745..83f55101029b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py index 146eee9741d7..ade464360a07 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +8,11 @@ from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import CommandTerm from isaaclab.markers import VisualizationMarkers diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py index 8501c00116da..e3c83882a3f5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py index c1a8c0f0d66c..148046f012c7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py @@ -1,14 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.envs import mdp from isaaclab.managers import ManagerTermBase, SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py index b48e4bcfb5c5..604c74320b01 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import ManagerTermBase, SceneEntityCfg from isaaclab.utils.math import quat_apply, quat_apply_inverse, quat_inv, quat_mul, subtract_frame_transforms diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py index 9a6170f1e4f9..a6ddab0f9081 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import RigidObject from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import ContactSensor diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py index 3ef9cf14b0ab..91bf2d0e3aaf 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py index f7b8e9db59b4..409abc5931d8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py @@ -1,19 +1,19 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 hashlib import logging + import numpy as np import torch import trimesh from trimesh.sample import sample_surface -import isaacsim.core.utils.prims as prim_utils from pxr import UsdGeom -from isaaclab.sim.utils import get_all_matching_child_prims +import isaaclab.sim as sim_utils # ---- module-scope caches ---- _PRIM_SAMPLE_CACHE: dict[tuple[str, int], np.ndarray] = {} # (prim_hash, num_points) -> (N,3) in root frame @@ -40,19 +40,21 @@ def sample_object_point_cloud(num_envs: int, num_points: int, prim_path: str, de """ points = torch.zeros((num_envs, num_points, 3), dtype=torch.float32, device=device) xform_cache = UsdGeom.XformCache() + # Obtain stage handle + stage = sim_utils.get_current_stage() for i in range(num_envs): # Resolve prim path obj_path = prim_path.replace(".*", str(i)) # Gather prims - prims = get_all_matching_child_prims( + prims = sim_utils.get_all_matching_child_prims( obj_path, predicate=lambda p: p.GetTypeName() in ("Mesh", "Cube", "Sphere", "Cylinder", "Capsule", "Cone") ) if not prims: raise KeyError(f"No valid prims under {obj_path}") - object_prim = prim_utils.get_prim_at_path(obj_path) + object_prim = stage.GetPrimAtPath(obj_path) world_root = xform_cache.GetLocalToWorldTransform(object_prim) # hash each child prim by its rel transform + geometry diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/__init__.py index 3e71ffde4f75..5dfbb3b40111 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/__init__.py index bf6df5ef83ae..c586540a04d2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py index 353f0002c3bd..9f53828ec4b6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rl_games_ppo_cfg.yaml index cdfed76bd258..2fa70902ab61 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rsl_rl_ppo_cfg.py index 4cbe6266f240..b1d3d4be1756 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/skrl_ppo_cfg.yaml index 6e12c4940faa..f61e7f50132d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/allegro_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/allegro_env_cfg.py index 78add19d4f93..7223ce0234fe 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/allegro_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/config/allegro_hand/allegro_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/inhand_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/inhand_env_cfg.py index 41c835b750c4..71594ae210d8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/inhand_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/inhand_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/__init__.py index 3102fbd96822..0fafe4500367 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/__init__.py index 01cf4ac656d4..ab3a9cf3e11c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/commands_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/commands_cfg.py index 4f652f4580fb..8c020137c0c9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/commands_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/commands_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/orientation_command.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/orientation_command.py index 191311ad8e0a..3f116a48c497 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/orientation_command.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/commands/orientation_command.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +7,11 @@ from __future__ import annotations -import torch from collections.abc import Sequence from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import RigidObject from isaaclab.managers import CommandTerm diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/events.py index 50b767c36f1c..dad2e88107e8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/events.py @@ -1,16 +1,16 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Functions specific to the in-hand dexterous manipulation environments.""" - from __future__ import annotations -import torch from typing import TYPE_CHECKING, Literal +import torch + from isaaclab.assets import Articulation from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg from isaaclab.utils.math import sample_uniform diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/observations.py index dfe99265362e..e90597925634 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Functions specific to the in-hand dexterous manipulation environments.""" -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import RigidObject from isaaclab.envs import ManagerBasedRLEnv diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/rewards.py index 01eb9aa6daea..f928b92fb367 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Functions specific to the in-hand dexterous manipulation environments.""" -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import RigidObject from isaaclab.envs import ManagerBasedRLEnv diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/terminations.py index 2630e4fe7fb7..1d4f36f1e62b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/inhand/mdp/terminations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Functions specific to the in-hand dexterous manipulation environments.""" -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.envs import ManagerBasedRLEnv from isaaclab.managers import SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/__init__.py index 2cec57c1cc93..1211ee056649 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/__init__.py index 2cec57c1cc93..1211ee056649 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/__init__.py index be28a9c575eb..d72fd6ebb596 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/__init__.py @@ -1,9 +1,8 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 gymnasium as gym -import os from . import agents @@ -72,7 +71,7 @@ entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ "env_cfg_entry_point": f"{__name__}.ik_rel_env_cfg:FrankaCubeLiftEnvCfg", - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc.json"), + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc.json", }, disable_env_checker=True, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rl_games_ppo_cfg.yaml index f61ffa6d3651..77360c1b91bb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rsl_rl_ppo_cfg.py index 067425a74d48..7a94614d8c97 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml index 91ae4f0d9f00..593de544a83a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/sb3_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/skrl_ppo_cfg.yaml index 5ddcf1713e75..3f39d0c4afc4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_abs_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_abs_env_cfg.py index 9fe910c8231e..9b9441a22341 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_abs_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_abs_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_rel_env_cfg.py index 89421e0848bc..5e5c95e7d472 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/ik_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/joint_pos_env_cfg.py index ac05be4caf57..5c5754c53e43 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/franka/joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/__init__.py new file mode 100644 index 000000000000..40ce61a45f83 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/__init__.py @@ -0,0 +1,38 @@ +# 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 gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +## +# Joint Position Control +## + +gym.register( + id="Isaac-Lift-Cube-OpenArm-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmCubeLiftEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmLiftCubePPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + }, + disable_env_checker=True, +) + +gym.register( + id="Isaac-Lift-Cube-OpenArm-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmCubeLiftEnvCfg_PLAY", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmLiftCubePPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + }, + disable_env_checker=True, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 000000000000..3363b1ea7348 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,85 @@ +# 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 + + +params: + seed: 42 + + # environment wrapper clipping + env: + clip_observations: 100.0 + clip_actions: 100.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [256, 128, 64] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: openarm_lift + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: True + normalize_value: True + value_bootstrap: False + num_actors: -1 + reward_shaper: + scale_value: 0.01 + normalize_advantage: True + gamma: 0.99 + tau: 0.95 + learning_rate: 1e-4 + lr_schedule: adaptive + schedule_type: legacy + kl_threshold: 0.01 + score_to_win: 100000000 + max_epochs: 1500 + save_best_after: 100 + save_frequency: 50 + print_stats: True + grad_norm: 1.0 + entropy_coef: 0.001 + truncate_grads: True + e_clip: 0.2 + horizon_length: 24 + minibatch_size: 24576 + mini_epochs: 8 + critic_coef: 4 + clip_value: True + clip_actions: False + seq_len: 4 + bounds_loss_coef: 0.0001 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 000000000000..f079552f1f4a --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,38 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from isaaclab.utils import configclass + +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class OpenArmLiftCubePPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 24 + max_iterations = 2000 + save_interval = 50 + experiment_name = "openarm_lift" + empirical_normalization = False + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_hidden_dims=[256, 128, 64], + critic_hidden_dims=[256, 128, 64], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.006, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-4, + schedule="adaptive", + gamma=0.98, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/joint_pos_env_cfg.py new file mode 100644 index 000000000000..b5f29f1fc32f --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/joint_pos_env_cfg.py @@ -0,0 +1,101 @@ +# 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 math + +from isaaclab.assets import RigidObjectCfg +from isaaclab.sensors import FrameTransformerCfg +from isaaclab.sim.schemas.schemas_cfg import RigidBodyPropertiesCfg +from isaaclab.sim.spawners.from_files.from_files_cfg import UsdFileCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from isaaclab_tasks.manager_based.manipulation.lift import mdp +from isaaclab_tasks.manager_based.manipulation.lift.config.openarm.lift_openarm_env_cfg import LiftEnvCfg + +from isaaclab_assets.robots.openarm import OPENARM_UNI_CFG + +## +# Pre-defined configs +## +from isaaclab.markers.config import FRAME_MARKER_CFG # isort: skip + + +@configclass +class OpenArmCubeLiftEnvCfg(LiftEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + + # Set OpenArm as robot + self.scene.robot = OPENARM_UNI_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # Set actions for the specific robot type (OpenArm) + self.actions.arm_action = mdp.JointPositionActionCfg( + asset_name="robot", + joint_names=[ + "openarm_joint.*", + ], + scale=0.5, + use_default_offset=True, + ) + + self.actions.gripper_action = mdp.BinaryJointPositionActionCfg( + asset_name="robot", + joint_names=["openarm_finger_joint.*"], + open_command_expr={"openarm_finger_joint.*": 0.044}, + close_command_expr={"openarm_finger_joint.*": 0.0}, + ) + + # Set the body name for the end effector + self.commands.object_pose.body_name = "openarm_hand" + self.commands.object_pose.ranges.pitch = (math.pi / 2, math.pi / 2) + + # Set Cube as object + self.scene.object = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Object", + init_state=RigidObjectCfg.InitialStateCfg(pos=[0.4, 0, 0.055], rot=[1, 0, 0, 0]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", + scale=(0.8, 0.8, 0.8), + rigid_props=RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=1, + max_angular_velocity=1000.0, + max_linear_velocity=1000.0, + max_depenetration_velocity=5.0, + disable_gravity=False, + ), + ), + ) + + # Listens to the required transforms + marker_cfg = FRAME_MARKER_CFG.copy() + marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) + marker_cfg.prim_path = "/Visuals/FrameTransformer" + self.scene.ee_frame = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/openarm_link0", + debug_vis=False, + visualizer_cfg=marker_cfg, + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/openarm_ee_tcp", + name="end_effector", + ), + ], + ) + + +@configclass +class OpenArmCubeLiftEnvCfg_PLAY(OpenArmCubeLiftEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + # make a smaller scene for play + self.scene.num_envs = 50 + self.scene.env_spacing = 2.5 + # disable randomization for play + self.observations.policy.enable_corruption = False diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/lift_openarm_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/lift_openarm_env_cfg.py new file mode 100644 index 000000000000..491b713c14f9 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/config/openarm/lift_openarm_env_cfg.py @@ -0,0 +1,240 @@ +# 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 + +""" +We modified parts of the environment, such as the target's position and orientation, +as well as certain object properties, to better suit the smaller robot. +""" + +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg, DeformableObjectCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.managers import CurriculumTermCfg as CurrTerm +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 RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sensors.frame_transformer.frame_transformer_cfg import FrameTransformerCfg +from isaaclab.sim.spawners.from_files.from_files_cfg import GroundPlaneCfg, UsdFileCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from ... import mdp + +## +# Scene definition +## + + +@configclass +class ObjectTableSceneCfg(InteractiveSceneCfg): + """Configuration for the lift scene with a robot and a object. + This is the abstract base implementation, the exact scene is defined in the derived classes + which need to set the target object, robot and end-effector frames + """ + + # robots: will be populated by agent env cfg + robot: ArticulationCfg = MISSING + # end-effector sensor: will be populated by agent env cfg + ee_frame: FrameTransformerCfg = MISSING + # target object: will be populated by agent env cfg + object: RigidObjectCfg | DeformableObjectCfg = MISSING + + # Table + table = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Table", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0.5, 0, 0], rot=[0.707, 0, 0, 0.707]), + spawn=UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd"), + ) + + # plane + plane = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0, 0, -1.05]), + spawn=GroundPlaneCfg(), + ) + + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0), + ) + + +## +# MDP settings +## + + +@configclass +class CommandsCfg: + """Command terms for the MDP.""" + + object_pose = mdp.UniformPoseCommandCfg( + asset_name="robot", + body_name=MISSING, # will be set by agent env cfg + resampling_time_range=(5.0, 5.0), + debug_vis=True, + ranges=mdp.UniformPoseCommandCfg.Ranges( + pos_x=(0.2, 0.4), + pos_y=(-0.2, 0.2), + pos_z=(0.15, 0.4), + roll=(0.0, 0.0), + pitch=(0.0, 0.0), + yaw=(0.0, 0.0), + ), + ) + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + # will be set by agent env cfg + arm_action: mdp.JointPositionActionCfg | mdp.DifferentialInverseKinematicsActionCfg = MISSING + gripper_action: mdp.BinaryJointPositionActionCfg = MISSING + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + joint_pos = ObsTerm( + func=mdp.joint_pos_rel, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["openarm_joint.*", "openarm_finger_joint.*"])}, + ) + joint_vel = ObsTerm( + func=mdp.joint_vel_rel, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["openarm_joint.*", "openarm_finger_joint.*"])}, + ) + object_position = ObsTerm(func=mdp.object_position_in_robot_root_frame) + target_object_position = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"}) + actions = ObsTerm(func=mdp.last_action) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + + # observation groups + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + reset_all = EventTerm(func=mdp.reset_scene_to_default, mode="reset") + + reset_object_position = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": {"x": (-0.1, 0.1), "y": (-0.25, 0.25), "z": (0.0, 0.0)}, + "velocity_range": {}, + "asset_cfg": SceneEntityCfg("object", body_names="Object"), + }, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + reaching_object = RewTerm(func=mdp.object_ee_distance, params={"std": 0.1}, weight=1.1) + + lifting_object = RewTerm(func=mdp.object_is_lifted, params={"minimal_height": 0.04}, weight=15.0) + + object_goal_tracking = RewTerm( + func=mdp.object_goal_distance, + params={"std": 0.3, "minimal_height": 0.04, "command_name": "object_pose"}, + weight=16.0, + ) + + object_goal_tracking_fine_grained = RewTerm( + func=mdp.object_goal_distance, + params={"std": 0.05, "minimal_height": 0.04, "command_name": "object_pose"}, + weight=5.0, + ) + + # action penalty + action_rate = RewTerm(func=mdp.action_rate_l2, weight=-1e-4) + + joint_vel = RewTerm( + func=mdp.joint_vel_l2, + weight=-1e-4, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["openarm_joint.*", "openarm_finger_joint.*"])}, + ) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + object_dropping = DoneTerm( + func=mdp.root_height_below_minimum, + params={"minimum_height": -0.05, "asset_cfg": SceneEntityCfg("object")}, + ) + + +@configclass +class CurriculumCfg: + """Curriculum terms for the MDP.""" + + action_rate = CurrTerm( + func=mdp.modify_reward_weight, + params={"term_name": "action_rate", "weight": -1e-1, "num_steps": 10000}, + ) + + joint_vel = CurrTerm( + func=mdp.modify_reward_weight, + params={"term_name": "joint_vel", "weight": -1e-1, "num_steps": 10000}, + ) + + +## +# Environment configuration +## + + +@configclass +class LiftEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the lifting environment.""" + + # Scene settings + scene: ObjectTableSceneCfg = ObjectTableSceneCfg(num_envs=4096, env_spacing=2.5) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + curriculum: CurriculumCfg = CurriculumCfg() + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 2 + self.episode_length_s = 5.0 + # simulation settings + self.sim.dt = 0.01 # 100Hz + self.sim.render_interval = self.decimation + + self.sim.physx.bounce_threshold_velocity = 0.01 + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 16 * 1024 + self.sim.physx.friction_correlation_distance = 0.00625 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/lift_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/lift_env_cfg.py index 3a4f458854d5..272661bda61d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/lift_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/lift_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/__init__.py index 4ad937d76ef9..f3dd0fecdf8e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/observations.py index 97bf9f8d02a6..8654933a9aae 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import RigidObject from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import subtract_frame_transforms diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/rewards.py index 799e6d4ad2fe..34e60773a068 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import RigidObject from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import FrameTransformer diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/terminations.py index 5229621c069b..68fe0e011b85 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import RigidObject from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import combine_frame_transforms diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/__init__.py index 740a487b2a5e..7f2bd7d0f707 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/__init__.py @@ -1,26 +1,18 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 gymnasium as gym -import os -from . import ( - agents, - exhaustpipe_gr1t2_pink_ik_env_cfg, - nutpour_gr1t2_pink_ik_env_cfg, - pickplace_gr1t2_env_cfg, - pickplace_gr1t2_waist_enabled_env_cfg, - pickplace_unitree_g1_inspire_hand_env_cfg, -) +from . import agents gym.register( id="Isaac-PickPlace-GR1T2-Abs-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": pickplace_gr1t2_env_cfg.PickPlaceGR1T2EnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + "env_cfg_entry_point": f"{__name__}.pickplace_gr1t2_env_cfg:PickPlaceGR1T2EnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_low_dim.json", }, disable_env_checker=True, ) @@ -29,8 +21,8 @@ id="Isaac-NutPour-GR1T2-Pink-IK-Abs-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": nutpour_gr1t2_pink_ik_env_cfg.NutPourGR1T2PinkIKEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_image_nut_pouring.json"), + "env_cfg_entry_point": f"{__name__}.nutpour_gr1t2_pink_ik_env_cfg:NutPourGR1T2PinkIKEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_image_nut_pouring.json", }, disable_env_checker=True, ) @@ -39,8 +31,8 @@ id="Isaac-ExhaustPipe-GR1T2-Pink-IK-Abs-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": exhaustpipe_gr1t2_pink_ik_env_cfg.ExhaustPipeGR1T2PinkIKEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_image_exhaust_pipe.json"), + "env_cfg_entry_point": f"{__name__}.exhaustpipe_gr1t2_pink_ik_env_cfg:ExhaustPipeGR1T2PinkIKEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_image_exhaust_pipe.json", }, disable_env_checker=True, ) @@ -49,8 +41,8 @@ id="Isaac-PickPlace-GR1T2-WaistEnabled-Abs-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": pickplace_gr1t2_waist_enabled_env_cfg.PickPlaceGR1T2WaistEnabledEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + "env_cfg_entry_point": f"{__name__}.pickplace_gr1t2_waist_enabled_env_cfg:PickPlaceGR1T2WaistEnabledEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_low_dim.json", }, disable_env_checker=True, ) @@ -59,8 +51,8 @@ id="Isaac-PickPlace-G1-InspireFTP-Abs-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": pickplace_unitree_g1_inspire_hand_env_cfg.PickPlaceG1InspireFTPEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + "env_cfg_entry_point": f"{__name__}.pickplace_unitree_g1_inspire_hand_env_cfg:PickPlaceG1InspireFTPEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_low_dim.json", }, disable_env_checker=True, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_base_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_base_env_cfg.py index 2d7a69653faf..6e14d2e1fdd2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_base_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_base_env_cfg.py @@ -1,22 +1,22 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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 tempfile -import torch from dataclasses import MISSING +import torch + import isaaclab.envs.mdp as base_mdp import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg from isaaclab.devices.openxr import XrCfg from isaaclab.envs import ManagerBasedRLEnvCfg -from isaaclab.managers import ActionTermCfg +from isaaclab.managers import 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.managers import TerminationTermCfg as DoneTerm from isaaclab.scene import InteractiveSceneCfg from isaaclab.sensors import CameraCfg @@ -36,6 +36,7 @@ ## @configclass class ObjectTableSceneCfg(InteractiveSceneCfg): + """Configuration for the GR1T2 Exhaust Pipe Base Scene.""" # Table table = AssetBaseCfg( @@ -275,44 +276,48 @@ class ExhaustPipeGR1T2BaseEnvCfg(ManagerBasedRLEnvCfg): # Idle action to hold robot in default pose # Action format: [left arm pos (3), left arm quat (4), right arm pos (3), # right arm quat (4), left/right hand joint pos (22)] - idle_action = torch.tensor([[ - -0.2909, - 0.2778, - 1.1247, - 0.5253, - 0.5747, - -0.4160, - 0.4699, - 0.22878, - 0.2536, - 1.0953, - 0.5, - 0.5, - -0.5, - 0.5, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ]]) + idle_action = torch.tensor( + [ + [ + -0.2909, + 0.2778, + 1.1247, + 0.5253, + 0.5747, + -0.4160, + 0.4699, + 0.22878, + 0.2536, + 1.0953, + 0.5, + 0.5, + -0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ] + ) def __post_init__(self): """Post initialization.""" @@ -323,9 +328,9 @@ def __post_init__(self): self.sim.dt = 1 / 100 self.sim.render_interval = 2 - # # Set settings for camera rendering - self.rerender_on_reset = True - self.sim.render.antialiasing_mode = "OFF" # disable dlss + # Set settings for camera rendering + self.num_rerenders_on_reset = 3 + self.sim.render.antialiasing_mode = "DLAA" # Use DLAA for higher quality rendering # List of image observations in policy observations self.image_obs_list = ["robot_pov_cam"] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py index 01feeab1cc2e..66ebfcad8a18 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/exhaustpipe_gr1t2_pink_ik_env_cfg.py @@ -1,11 +1,12 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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 carb from pink.tasks import DampingTask, FrameTask +import carb + import isaaclab.controllers.utils as ControllerUtils from isaaclab.controllers.pink_ik import NullSpacePostureTask, PinkIKControllerCfg from isaaclab.devices import DevicesCfg @@ -80,7 +81,8 @@ def __post_init__(self): base_link_name="base_link", num_hand_joints=22, show_ik_warnings=False, - fail_on_joint_limit_violation=False, # Determines whether to pink solver will fail due to a joint limit violation + # Determines whether Pink IK solver will fail due to a joint limit violation + fail_on_joint_limit_violation=False, variable_input_tasks=[ FrameTask( "GR1T2_fourier_hand_6dof_left_hand_pitch_link", diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/__init__.py index 3ffbe30fc5bb..555bfb7cbe8f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/observations.py index b4dfcb6829f1..01e52e73f242 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + if TYPE_CHECKING: from isaaclab.envs import ManagerBasedRLEnv diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/pick_place_events.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/pick_place_events.py index eed406274e2e..ca1fd940fea8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/pick_place_events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/pick_place_events.py @@ -1,13 +1,14 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.managers import SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/terminations.py index 477552bbdbae..6252b9c67a21 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import RigidObject from isaaclab.managers import SceneEntityCfg @@ -190,9 +191,12 @@ def task_done_exhaust_pipe( env: The RL environment instance. blue_exhaust_pipe_cfg: Configuration for the blue exhaust pipe entity. blue_sorting_bin_cfg: Configuration for the blue sorting bin entity. - max_blue_exhaust_to_bin_x: Maximum x position of the blue exhaust pipe relative to the blue sorting bin for task completion. - max_blue_exhaust_to_bin_y: Maximum y position of the blue exhaust pipe relative to the blue sorting bin for task completion. - max_blue_exhaust_to_bin_z: Maximum z position of the blue exhaust pipe relative to the blue sorting bin for task completion. + max_blue_exhaust_to_bin_x: Maximum x position of the blue exhaust pipe + relative to the blue sorting bin for task completion. + max_blue_exhaust_to_bin_y: Maximum y position of the blue exhaust pipe + relative to the blue sorting bin for task completion. + max_blue_exhaust_to_bin_z: Maximum z position of the blue exhaust pipe + relative to the blue sorting bin for task completion. Returns: Boolean tensor indicating which environments have completed the task. diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_base_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_base_env_cfg.py index 6aaf5defb388..01caf58a8af2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_base_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_base_env_cfg.py @@ -1,22 +1,22 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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 tempfile -import torch from dataclasses import MISSING +import torch + import isaaclab.envs.mdp as base_mdp import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg from isaaclab.devices.openxr import XrCfg from isaaclab.envs import ManagerBasedRLEnvCfg -from isaaclab.managers import ActionTermCfg +from isaaclab.managers import 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.managers import TerminationTermCfg as DoneTerm from isaaclab.scene import InteractiveSceneCfg from isaaclab.sensors import CameraCfg @@ -36,6 +36,7 @@ ## @configclass class ObjectTableSceneCfg(InteractiveSceneCfg): + """Configuration for the GR1T2 Nut Pour Base Scene.""" # Table table = AssetBaseCfg( @@ -310,44 +311,48 @@ class NutPourGR1T2BaseEnvCfg(ManagerBasedRLEnvCfg): # Idle action to hold robot in default pose # Action format: [left arm pos (3), left arm quat (4), right arm pos (3), # right arm quat (4), left/right hand joint pos (22)] - idle_action = torch.tensor([[ - -0.22878, - 0.2536, - 1.0953, - 0.5, - 0.5, - -0.5, - 0.5, - 0.22878, - 0.2536, - 1.0953, - 0.5, - 0.5, - -0.5, - 0.5, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ]]) + idle_action = torch.tensor( + [ + [ + -0.22878, + 0.2536, + 1.0953, + 0.5, + 0.5, + -0.5, + 0.5, + 0.22878, + 0.2536, + 1.0953, + 0.5, + 0.5, + -0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ] + ) def __post_init__(self): """Post initialization.""" @@ -359,8 +364,8 @@ def __post_init__(self): self.sim.render_interval = 2 # Set settings for camera rendering - self.rerender_on_reset = True - self.sim.render.antialiasing_mode = "OFF" # disable dlss + self.num_rerenders_on_reset = 3 + self.sim.render.antialiasing_mode = "DLAA" # Use DLAA for higher quality rendering # List of image observations in policy observations self.image_obs_list = ["robot_pov_cam"] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py index 6dcdd9a1e8fc..818fba1fc805 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/nutpour_gr1t2_pink_ik_env_cfg.py @@ -1,11 +1,12 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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 carb from pink.tasks import DampingTask, FrameTask +import carb + import isaaclab.controllers.utils as ControllerUtils from isaaclab.controllers.pink_ik import NullSpacePostureTask, PinkIKControllerCfg from isaaclab.devices import DevicesCfg @@ -78,7 +79,8 @@ def __post_init__(self): base_link_name="base_link", num_hand_joints=22, show_ik_warnings=False, - fail_on_joint_limit_violation=False, # Determines whether to pink solver will fail due to a joint limit violation + # Determines whether Pink IK solver will fail due to a joint limit violation + fail_on_joint_limit_violation=False, variable_input_tasks=[ FrameTask( "GR1T2_fourier_hand_6dof_left_hand_pitch_link", diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_env_cfg.py index 4b073b35a3f7..ba6c5d385135 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_env_cfg.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 tempfile + import torch +from pink.tasks import DampingTask, FrameTask import carb -from pink.tasks import DampingTask, FrameTask import isaaclab.controllers.utils as ControllerUtils import isaaclab.envs.mdp as base_mdp @@ -39,6 +40,7 @@ ## @configclass class ObjectTableSceneCfg(InteractiveSceneCfg): + """Configuration for the GR1T2 Pick Place Base Scene.""" # Table packing_table = AssetBaseCfg( @@ -171,7 +173,8 @@ class ActionsCfg: base_link_name="base_link", num_hand_joints=22, show_ik_warnings=False, - fail_on_joint_limit_violation=False, # Determines whether to pink solver will fail due to a joint limit violation + # Determines whether Pink IK solver will fail due to a joint limit violation + fail_on_joint_limit_violation=False, variable_input_tasks=[ FrameTask( "GR1T2_fourier_hand_6dof_left_hand_pitch_link", @@ -327,44 +330,46 @@ class PickPlaceGR1T2EnvCfg(ManagerBasedRLEnvCfg): # Idle action to hold robot in default pose # Action format: [left arm pos (3), left arm quat (4), right arm pos (3), right arm quat (4), # left hand joint pos (11), right hand joint pos (11)] - idle_action = torch.tensor([ - -0.22878, - 0.2536, - 1.0953, - 0.5, - 0.5, - -0.5, - 0.5, - 0.22878, - 0.2536, - 1.0953, - 0.5, - 0.5, - -0.5, - 0.5, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ]) + idle_action = torch.tensor( + [ + -0.22878, + 0.2536, + 1.0953, + 0.5, + 0.5, + -0.5, + 0.5, + 0.22878, + 0.2536, + 1.0953, + 0.5, + 0.5, + -0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ) def __post_init__(self): """Post initialization.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py index 30b17e89493a..23ed8d984bcd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_gr1t2_waist_enabled_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_unitree_g1_inspire_hand_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_unitree_g1_inspire_hand_env_cfg.py index a557911498a0..85af79e7fb19 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_unitree_g1_inspire_hand_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_place/pickplace_unitree_g1_inspire_hand_env_cfg.py @@ -1,12 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 tempfile + import torch +from pink.tasks import FrameTask import carb -from pink.tasks import FrameTask import isaaclab.controllers.utils as ControllerUtils import isaaclab.envs.mdp as base_mdp @@ -39,6 +40,7 @@ ## @configclass class ObjectTableSceneCfg(InteractiveSceneCfg): + """Configuration for the Unitree G1 Inspire Hand Pick Place Base Scene.""" # Table packing_table = AssetBaseCfg( @@ -316,47 +318,49 @@ class PickPlaceG1InspireFTPEnvCfg(ManagerBasedRLEnvCfg): # Idle action to hold robot in default pose # Action format: [left arm pos (3), left arm quat (4), right arm pos (3), right arm quat (4), # left hand joint pos (12), right hand joint pos (12)] - idle_action = torch.tensor([ - # 14 hand joints for EEF control - -0.1487, - 0.2038, - 1.0952, - 0.707, - 0.0, - 0.0, - 0.707, - 0.1487, - 0.2038, - 1.0952, - 0.707, - 0.0, - 0.0, - 0.707, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ]) + idle_action = torch.tensor( + [ + # 14 hand joints for EEF control + -0.1487, + 0.2038, + 1.0952, + 0.707, + 0.0, + 0.0, + 0.707, + 0.1487, + 0.2038, + 1.0952, + 0.707, + 0.0, + 0.0, + 0.707, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ) def __post_init__(self): """Post initialization.""" @@ -385,8 +389,10 @@ def __post_init__(self): # number of joints in both hands num_open_xr_hand_joints=2 * 26, sim_device=self.sim.device, - # Please confirm that self.actions.pink_ik_cfg.hand_joint_names is consistent with robot.joint_names[-24:] - # The order of the joints does matter as it will be used for converting pink_ik actions to final control actions in IsaacLab. + # Please confirm that self.actions.pink_ik_cfg.hand_joint_names is + # consistent with robot.joint_names[-24:] + # The order of the joints does matter as it will be used for + # converting pink_ik actions to final control actions in IsaacLab. hand_joint_names=self.actions.pink_ik_cfg.hand_joint_names, ), ], diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py index d2bbb58b0cb0..696e138c3e42 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py index d2bbb58b0cb0..696e138c3e42 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py index 6941186bea49..7b99bd4a5d34 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py index 18d8ccdf1cbf..ffe842b0202a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -209,6 +209,7 @@ def __post_init__(self): class RmpFlowAgibotPlaceToy2BoxEnvCfg(PlaceToy2BoxEnvCfg): + """Configuration for the Agibot Place Toy2Box RMP Rel Environment.""" def __post_init__(self): # post init of parent diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py index 6689a9cb1540..14841036d66e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -151,6 +151,7 @@ class TerminationsCfg: class RmpFlowAgibotPlaceUprightMugEnvCfg(place_toy2box_rmp_rel_env_cfg.PlaceToy2BoxEnvCfg): + """Configuration for the Agibot Place Upright Mug RMP Rel Environment.""" def __post_init__(self): # post init of parent diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py index f394d204c700..41f76fcdb1bb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py index 18870db2cad1..b0a9107beca1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING, Literal +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import SceneEntityCfg @@ -41,7 +42,7 @@ def object_poses_in_base_frame( return pos_object_base elif return_key == "quat": return quat_object_base - elif return_key is None: + else: return torch.cat((pos_object_base, quat_object_base), dim=1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py index 9768321ef13a..cf7248d29348 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/__init__.py index 629efd934641..be11b529e2c2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/__init__.py index d94f3eb4e51e..acf853fcf647 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/__init__.py index 8c159b81eb0b..47158f64a650 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rl_games_ppo_cfg.yaml index 5945fc0b45d1..09e4f9d48b59 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rsl_rl_ppo_cfg.py index 24bea7c5ac14..ede70559fd56 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/skrl_ppo_cfg.yaml index d6cf3c8dd251..986b35fff6b3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_abs_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_abs_env_cfg.py index 83a85ec28394..b090e568965e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_abs_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_abs_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_rel_env_cfg.py index 8099386a381b..024a42270d85 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/ik_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/joint_pos_env_cfg.py index 2c5d573ff1f8..a848ddb87667 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/osc_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/osc_env_cfg.py index bfeefcda3bf2..e612439fda70 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/osc_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/franka/osc_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/__init__.py new file mode 100644 index 000000000000..6cd284b1838e --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/__init__.py @@ -0,0 +1,38 @@ +# 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 gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +## +# Joint Position Control +## + +gym.register( + id="Isaac-Reach-OpenArm-Bi-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmReachEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmReachPPORunnerCfg", + }, +) + +gym.register( + id="Isaac-Reach-OpenArm-Bi-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmReachEnvCfg_PLAY", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmReachPPORunnerCfg", + }, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 000000000000..71526744500e --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,83 @@ +# 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 + +params: + seed: 42 + + # environment wrapper clipping + env: + clip_observations: 100.0 + clip_actions: 100.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [64, 64] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: openarm_bi_reach + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: True + normalize_value: True + value_bootstrap: True + num_actors: -1 + reward_shaper: + scale_value: 1.0 + normalize_advantage: True + gamma: 0.99 + tau: 0.95 + learning_rate: 1e-3 + lr_schedule: adaptive + schedule_type: legacy + kl_threshold: 0.01 + score_to_win: 10000 + max_epochs: 1000 + save_best_after: 200 + save_frequency: 100 + print_stats: True + grad_norm: 1.0 + entropy_coef: 0.01 + truncate_grads: True + e_clip: 0.2 + horizon_length: 24 + minibatch_size: 24576 + mini_epochs: 5 + critic_coef: 2 + clip_value: True + clip_actions: False + bounds_loss_coef: 0.0001 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 000000000000..d1dd736a2ed7 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,39 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class OpenArmReachPPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 24 + max_iterations = 550 + save_interval = 50 + experiment_name = "openarm_bi_reach" + run_name = "" + resume = False + empirical_normalization = False + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_hidden_dims=[64, 64], + critic_hidden_dims=[64, 64], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.001, + num_learning_epochs=8, + num_mini_batches=4, + learning_rate=1.0e-2, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/joint_pos_env_cfg.py new file mode 100644 index 000000000000..6b17b4174cb0 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/joint_pos_env_cfg.py @@ -0,0 +1,80 @@ +# 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 + +## +# Pre-defined configs +## + +from isaaclab.utils import configclass + +import isaaclab_tasks.manager_based.manipulation.reach.mdp as mdp +from isaaclab_tasks.manager_based.manipulation.reach.config.openarm.bimanual.reach_openarm_bi_env_cfg import ReachEnvCfg + +from isaaclab_assets.robots.openarm import OPENARM_BI_HIGH_PD_CFG + +## +# Environment configuration +## + + +@configclass +class OpenArmReachEnvCfg(ReachEnvCfg): + """Configuration for the Bimanual OpenArm Reach Environment.""" + + def __post_init__(self): + # post init of parent + super().__post_init__() + + # switch robot to OpenArm + self.scene.robot = OPENARM_BI_HIGH_PD_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # override rewards + self.rewards.left_end_effector_position_tracking.params["asset_cfg"].body_names = ["openarm_left_hand"] + self.rewards.left_end_effector_position_tracking_fine_grained.params["asset_cfg"].body_names = [ + "openarm_left_hand" + ] + self.rewards.left_end_effector_orientation_tracking.params["asset_cfg"].body_names = ["openarm_left_hand"] + + self.rewards.right_end_effector_position_tracking.params["asset_cfg"].body_names = ["openarm_right_hand"] + self.rewards.right_end_effector_position_tracking_fine_grained.params["asset_cfg"].body_names = [ + "openarm_right_hand" + ] + self.rewards.right_end_effector_orientation_tracking.params["asset_cfg"].body_names = ["openarm_right_hand"] + + # override actions + self.actions.left_arm_action = mdp.JointPositionActionCfg( + asset_name="robot", + joint_names=[ + "openarm_left_joint.*", + ], + scale=0.5, + use_default_offset=True, + ) + + self.actions.right_arm_action = mdp.JointPositionActionCfg( + asset_name="robot", + joint_names=[ + "openarm_right_joint.*", + ], + scale=0.5, + use_default_offset=True, + ) + + # override command generator body + # end-effector is along z-direction + self.commands.left_ee_pose.body_name = "openarm_left_hand" + self.commands.right_ee_pose.body_name = "openarm_right_hand" + + +@configclass +class OpenArmReachEnvCfg_PLAY(OpenArmReachEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + # make a smaller scene for play + self.scene.num_envs = 50 + self.scene.env_spacing = 2.5 + # disable randomization for play + self.observations.policy.enable_corruption = False diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/reach_openarm_bi_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/reach_openarm_bi_env_cfg.py new file mode 100644 index 000000000000..7ccdfa0f851b --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/bimanual/reach_openarm_bi_env_cfg.py @@ -0,0 +1,335 @@ +# 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 + +""" +We modified parts of the environment—such as the target’s position and orientation—to better suit the smaller robot. +""" + +import math +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.managers import ActionTermCfg as ActionTerm +from isaaclab.managers import CurriculumTermCfg as CurrTerm +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 RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.utils import configclass +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +import isaaclab_tasks.manager_based.manipulation.reach.mdp as mdp + +## +# Scene definition +## + + +@configclass +class ReachSceneCfg(InteractiveSceneCfg): + """Configuration for the scene with a robotic arm.""" + + # world + ground = AssetBaseCfg( + prim_path="/World/ground", + spawn=sim_utils.GroundPlaneCfg(), + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, 0)), + ) + + # robots + robot: ArticulationCfg = MISSING + + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=2500.0), + ) + + +## +# MDP settings +## + + +@configclass +class CommandsCfg: + """Command terms for the MDP.""" + + left_ee_pose = mdp.UniformPoseCommandCfg( + asset_name="robot", + body_name=MISSING, + resampling_time_range=(4.0, 4.0), + debug_vis=True, + ranges=mdp.UniformPoseCommandCfg.Ranges( + pos_x=(0.15, 0.3), + pos_y=(0.15, 0.25), + pos_z=(0.3, 0.5), + roll=(-math.pi / 6, math.pi / 6), + pitch=(3 * math.pi / 2, 3 * math.pi / 2), + yaw=(8 * math.pi / 9, 10 * math.pi / 9), + ), + ) + + right_ee_pose = mdp.UniformPoseCommandCfg( + asset_name="robot", + body_name=MISSING, + resampling_time_range=(4.0, 4.0), + debug_vis=True, + ranges=mdp.UniformPoseCommandCfg.Ranges( + pos_x=(0.15, 0.3), + pos_y=(-0.25, -0.15), + pos_z=(0.3, 0.5), + roll=(-math.pi / 6, math.pi / 6), + pitch=(3 * math.pi / 2, 3 * math.pi / 2), + yaw=(8 * math.pi / 9, 10 * math.pi / 9), + ), + ) + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + left_arm_action: ActionTerm = MISSING + right_arm_action: ActionTerm = MISSING + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + # observation terms (order preserved) + left_joint_pos = ObsTerm( + func=mdp.joint_pos_rel, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=[ + "openarm_left_joint.*", + ], + ) + }, + noise=Unoise(n_min=-0.01, n_max=0.01), + ) + + right_joint_pos = ObsTerm( + func=mdp.joint_pos_rel, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=[ + "openarm_right_joint.*", + ], + ) + }, + noise=Unoise(n_min=-0.01, n_max=0.01), + ) + + left_joint_vel = ObsTerm( + func=mdp.joint_vel_rel, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=[ + "openarm_left_joint.*", + ], + ) + }, + noise=Unoise(n_min=-0.01, n_max=0.01), + ) + right_joint_vel = ObsTerm( + func=mdp.joint_vel_rel, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=[ + "openarm_right_joint.*", + ], + ) + }, + noise=Unoise(n_min=-0.01, n_max=0.01), + ) + left_pose_command = ObsTerm(func=mdp.generated_commands, params={"command_name": "left_ee_pose"}) + right_pose_command = ObsTerm(func=mdp.generated_commands, params={"command_name": "right_ee_pose"}) + left_actions = ObsTerm(func=mdp.last_action, params={"action_name": "left_arm_action"}) + right_actions = ObsTerm(func=mdp.last_action, params={"action_name": "right_arm_action"}) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + + # observation groups + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + reset_robot_joints = EventTerm( + func=mdp.reset_joints_by_scale, + mode="reset", + params={ + "position_range": (0.5, 1.5), + "velocity_range": (0.0, 0.0), + }, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + # task terms + left_end_effector_position_tracking = RewTerm( + func=mdp.position_command_error, + weight=-0.2, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "command_name": "left_ee_pose", + }, + ) + + right_end_effector_position_tracking = RewTerm( + func=mdp.position_command_error, + weight=-0.25, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "command_name": "right_ee_pose", + }, + ) + + left_end_effector_position_tracking_fine_grained = RewTerm( + func=mdp.position_command_error_tanh, + weight=0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "std": 0.1, + "command_name": "left_ee_pose", + }, + ) + + right_end_effector_position_tracking_fine_grained = RewTerm( + func=mdp.position_command_error_tanh, + weight=0.2, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "std": 0.1, + "command_name": "right_ee_pose", + }, + ) + + left_end_effector_orientation_tracking = RewTerm( + func=mdp.orientation_command_error, + weight=-0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "command_name": "left_ee_pose", + }, + ) + + right_end_effector_orientation_tracking = RewTerm( + func=mdp.orientation_command_error, + weight=-0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "command_name": "right_ee_pose", + }, + ) + + # action penalty + action_rate = RewTerm(func=mdp.action_rate_l2, weight=-0.0001) + left_joint_vel = RewTerm( + func=mdp.joint_vel_l2, + weight=-0.0001, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=[ + "openarm_left_joint.*", + ], + ) + }, + ) + right_joint_vel = RewTerm( + func=mdp.joint_vel_l2, + weight=-0.0001, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=[ + "openarm_right_joint.*", + ], + ) + }, + ) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + +@configclass +class CurriculumCfg: + """Curriculum terms for the MDP.""" + + action_rate = CurrTerm( + func=mdp.modify_reward_weight, + params={"term_name": "action_rate", "weight": -0.005, "num_steps": 4500}, + ) + + left_joint_vel = CurrTerm( + func=mdp.modify_reward_weight, + params={"term_name": "left_joint_vel", "weight": -0.001, "num_steps": 4500}, + ) + + right_joint_vel = CurrTerm( + func=mdp.modify_reward_weight, + params={"term_name": "right_joint_vel", "weight": -0.001, "num_steps": 4500}, + ) + + +## +# Environment configuration +## + + +@configclass +class ReachEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the reach end-effector pose tracking environment.""" + + # Scene settings + scene: ReachSceneCfg = ReachSceneCfg(num_envs=4096, env_spacing=2.5) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + curriculum: CurriculumCfg = CurriculumCfg() + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 2 + self.sim.render_interval = self.decimation + self.episode_length_s = 24.0 + self.viewer.eye = (3.5, 3.5, 3.5) + # simulation settings + self.sim.dt = 1.0 / 60.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/__init__.py new file mode 100644 index 000000000000..d0003b608094 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/__init__.py @@ -0,0 +1,40 @@ +# 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 gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +## +# Joint Position Control +## + +gym.register( + id="Isaac-Reach-OpenArm-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmReachEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmReachPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Reach-OpenArm-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.joint_pos_env_cfg:OpenArmReachEnvCfg_PLAY", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OpenArmReachPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + }, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/__init__.py new file mode 100644 index 000000000000..460a30569089 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/__init__.py @@ -0,0 +1,4 @@ +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 000000000000..29349e1e389f --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,84 @@ +# 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 + + +params: + seed: 42 + + # environment wrapper clipping + env: + clip_observations: 100.0 + clip_actions: 100.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [64, 64] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: openarm_reach + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: True + normalize_value: True + value_bootstrap: True + num_actors: -1 + reward_shaper: + scale_value: 1.0 + normalize_advantage: True + gamma: 0.99 + tau: 0.95 + learning_rate: 1e-3 + lr_schedule: adaptive + schedule_type: legacy + kl_threshold: 0.01 + score_to_win: 10000 + max_epochs: 1000 + save_best_after: 200 + save_frequency: 100 + print_stats: True + grad_norm: 1.0 + entropy_coef: 0.01 + truncate_grads: True + e_clip: 0.2 + horizon_length: 24 + minibatch_size: 24576 + mini_epochs: 5 + critic_coef: 2 + clip_value: True + clip_actions: False + bounds_loss_coef: 0.0001 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 000000000000..4d43c3574195 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,40 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from isaaclab.utils import configclass + +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class OpenArmReachPPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 24 + max_iterations = 1000 + save_interval = 50 + experiment_name = "openarm_reach" + run_name = "" + resume = False + empirical_normalization = True + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_hidden_dims=[64, 64], + critic_hidden_dims=[64, 64], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.001, + num_learning_epochs=8, + num_mini_batches=4, + learning_rate=1.0e-2, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/skrl_ppo_cfg.yaml new file mode 100644 index 000000000000..93bd0e506bb4 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/agents/skrl_ppo_cfg.yaml @@ -0,0 +1,85 @@ +# 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 + +seed: 42 + + +# Models are instantiated using skrl's model instantiator utility +# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html +models: + separate: False + policy: # see gaussian_model parameters + class: GaussianMixin + clip_actions: False + clip_log_std: True + min_log_std: -20.0 + max_log_std: 2.0 + initial_log_std: 0.0 + network: + - name: net + input: STATES + layers: [64, 64] + activations: elu + output: ACTIONS + value: # see deterministic_model parameters + class: DeterministicMixin + clip_actions: False + network: + - name: net + input: STATES + layers: [64, 64] + activations: elu + output: ONE + + +# Rollout memory +# https://skrl.readthedocs.io/en/latest/api/memories/random.html +memory: + class: RandomMemory + memory_size: -1 # automatically determined (same as agent:rollouts) + + +# PPO agent configuration (field names are from PPO_DEFAULT_CONFIG) +# https://skrl.readthedocs.io/en/latest/api/agents/ppo.html +agent: + class: PPO + rollouts: 24 + learning_epochs: 5 + mini_batches: 4 + discount_factor: 0.99 + lambda: 0.95 + learning_rate: 1.0e-03 + learning_rate_scheduler: KLAdaptiveLR + learning_rate_scheduler_kwargs: + kl_threshold: 0.01 + state_preprocessor: RunningStandardScaler + state_preprocessor_kwargs: null + value_preprocessor: RunningStandardScaler + value_preprocessor_kwargs: null + random_timesteps: 0 + learning_starts: 0 + grad_norm_clip: 1.0 + ratio_clip: 0.2 + value_clip: 0.2 + clip_predicted_values: True + entropy_loss_scale: 0.01 + value_loss_scale: 1.0 + kl_threshold: 0.0 + rewards_shaper_scale: 1.0 + time_limit_bootstrap: False + # logging and checkpoint + experiment: + directory: "openarm_reach" + experiment_name: "" + write_interval: auto + checkpoint_interval: auto + + +# Sequential trainer +# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html +trainer: + class: SequentialTrainer + timesteps: 24000 + environment_info: log diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/joint_pos_env_cfg.py new file mode 100644 index 000000000000..2bfd6e326a5a --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/joint_pos_env_cfg.py @@ -0,0 +1,78 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.assets.articulation import ArticulationCfg +from isaaclab.utils import configclass + +import isaaclab_tasks.manager_based.manipulation.reach.mdp as mdp +from isaaclab_tasks.manager_based.manipulation.reach.config.openarm.unimanual.reach_openarm_uni_env_cfg import ( + ReachEnvCfg, +) + +## +# Pre-defined configs +## +from isaaclab_assets.robots.openarm import OPENARM_UNI_CFG + +## +# Environment configuration +## + + +@configclass +class OpenArmReachEnvCfg(ReachEnvCfg): + """Configuration for the single-arm OpenArm Reach Environment.""" + + def __post_init__(self): + # post init of parent + super().__post_init__() + + # switch robot to OpenArm + self.scene.robot = OPENARM_UNI_CFG.replace( + prim_path="{ENV_REGEX_NS}/Robot", + init_state=ArticulationCfg.InitialStateCfg( + joint_pos={ + "openarm_joint1": 1.57, + "openarm_joint2": 0.0, + "openarm_joint3": -1.57, + "openarm_joint4": 1.57, + "openarm_joint5": 0.0, + "openarm_joint6": 0.0, + "openarm_joint7": 0.0, + "openarm_finger_joint.*": 0.0, + }, # Close the gripper + ), + ) + + # override rewards + self.rewards.end_effector_position_tracking.params["asset_cfg"].body_names = ["openarm_hand"] + self.rewards.end_effector_position_tracking_fine_grained.params["asset_cfg"].body_names = ["openarm_hand"] + self.rewards.end_effector_orientation_tracking.params["asset_cfg"].body_names = ["openarm_hand"] + + # override actions + self.actions.arm_action = mdp.JointPositionActionCfg( + asset_name="robot", + joint_names=[ + "openarm_joint.*", + ], + scale=0.5, + use_default_offset=True, + ) + + # override command generator body + # end-effector is along z-direction + self.commands.ee_pose.body_name = "openarm_hand" + + +@configclass +class OpenArmReachEnvCfg_PLAY(OpenArmReachEnvCfg): + def __post_init__(self): + # post init of parent + super().__post_init__() + # make a smaller scene for play + self.scene.num_envs = 50 + self.scene.env_spacing = 2.5 + # disable randomization for play + self.observations.policy.enable_corruption = False diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/reach_openarm_uni_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/reach_openarm_uni_env_cfg.py new file mode 100644 index 000000000000..ed9bcbfc08be --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/openarm/unimanual/reach_openarm_uni_env_cfg.py @@ -0,0 +1,248 @@ +# 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 + +""" +We modified parts of the environment—such as the target’s position and orientation—to better suit the smaller robot. +""" + +import math +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.managers import ActionTermCfg as ActionTerm +from isaaclab.managers import CurriculumTermCfg as CurrTerm +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 RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +import isaaclab_tasks.manager_based.manipulation.reach.mdp as mdp + +## +# Scene definition +## + + +@configclass +class ReachSceneCfg(InteractiveSceneCfg): + """Configuration for the scene with a robotic arm.""" + + # world + ground = AssetBaseCfg( + prim_path="/World/ground", + spawn=sim_utils.GroundPlaneCfg(), + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, -1.05)), + ) + + table = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Table", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd", + ), + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.55, 0.0, 0.0), rot=(0.70711, 0.0, 0.0, 0.70711)), + ) + + # robots + robot: ArticulationCfg = MISSING + + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DomeLightCfg(color=(0.75, 0.75, 0.75), intensity=2500.0), + ) + + +## +# MDP settings +## + + +@configclass +class CommandsCfg: + """Command terms for the MDP.""" + + ee_pose = mdp.UniformPoseCommandCfg( + asset_name="robot", + body_name=MISSING, + resampling_time_range=(4.0, 4.0), + debug_vis=True, + ranges=mdp.UniformPoseCommandCfg.Ranges( + pos_x=(0.25, 0.35), + pos_y=(-0.2, 0.2), + pos_z=(0.3, 0.4), + roll=(-math.pi / 6, math.pi / 6), + pitch=(math.pi / 2, math.pi / 2), + yaw=(-math.pi / 9, math.pi / 9), + ), + ) + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + arm_action: ActionTerm = MISSING + gripper_action: ActionTerm | None = None + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + # observation terms (order preserved) + joint_pos = ObsTerm( + func=mdp.joint_pos_rel, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=["openarm_joint.*"], + ) + }, + noise=Unoise(n_min=-0.01, n_max=0.01), + ) + joint_vel = ObsTerm( + func=mdp.joint_vel_rel, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=["openarm_joint.*"], + ) + }, + noise=Unoise(n_min=-0.01, n_max=0.01), + ) + pose_command = ObsTerm(func=mdp.generated_commands, params={"command_name": "ee_pose"}) + actions = ObsTerm(func=mdp.last_action) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + + # observation groups + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + reset_robot_joints = EventTerm( + func=mdp.reset_joints_by_scale, + mode="reset", + params={ + "position_range": (0.5, 1.5), + "velocity_range": (0.0, 0.0), + }, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + # task terms + end_effector_position_tracking = RewTerm( + func=mdp.position_command_error, + weight=-0.2, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "command_name": "ee_pose", + }, + ) + end_effector_position_tracking_fine_grained = RewTerm( + func=mdp.position_command_error_tanh, + weight=0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "std": 0.1, + "command_name": "ee_pose", + }, + ) + end_effector_orientation_tracking = RewTerm( + func=mdp.orientation_command_error, + weight=-0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=MISSING), + "command_name": "ee_pose", + }, + ) + + # action penalty + action_rate = RewTerm(func=mdp.action_rate_l2, weight=-0.0001) + joint_vel = RewTerm( + func=mdp.joint_vel_l2, + weight=-0.0001, + params={ + "asset_cfg": SceneEntityCfg( + "robot", + joint_names=["openarm_joint.*"], + ) + }, + ) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + +@configclass +class CurriculumCfg: + """Curriculum terms for the MDP.""" + + action_rate = CurrTerm( + func=mdp.modify_reward_weight, + params={"term_name": "action_rate", "weight": -0.005, "num_steps": 4500}, + ) + + joint_vel = CurrTerm( + func=mdp.modify_reward_weight, + params={"term_name": "joint_vel", "weight": -0.001, "num_steps": 4500}, + ) + + +## +# Environment configuration +## + + +@configclass +class ReachEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the reach end-effector pose tracking environment.""" + + # Scene settings + scene: ReachSceneCfg = ReachSceneCfg(num_envs=4096, env_spacing=2.5) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + curriculum: CurriculumCfg = CurriculumCfg() + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 2 + self.sim.render_interval = self.decimation + self.episode_length_s = 12.0 + self.viewer.eye = (3.5, 3.5, 3.5) + # simulation settings + self.sim.dt = 1.0 / 60.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py index e9ca1de67056..fafe5b75820c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rl_games_ppo_cfg.yaml index 1961f08ca5b7..06a3c70554e3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rl_games_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rl_games_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rsl_rl_ppo_cfg.py index 1b55830a64ea..c445786c44c7 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/skrl_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/skrl_ppo_cfg.yaml index f14c8a6094b5..327a9b9ca80e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/skrl_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/agents/skrl_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/joint_pos_env_cfg.py index 25f1ea799d6b..6ddf935768b8 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/config/ur_10/joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/__init__.py index 99936340cac9..3fec83fe70af 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/rewards.py index c078bc3e5d4a..76f2fe36db41 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import RigidObject from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import combine_frame_transforms, quat_error_magnitude, quat_mul diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/reach_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/reach_env_cfg.py index 8890010a71be..bad88b401c7c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/reach_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/reach/reach_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/__init__.py index 236b2daab6e4..9e0ebd843653 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/__init__.py index 236b2daab6e4..9e0ebd843653 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/__init__.py index 0e3db6206b77..0c93d83ff1f1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/__init__.py @@ -1,23 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 gymnasium as gym -import os -from . import ( - agents, - bin_stack_ik_rel_env_cfg, - stack_ik_abs_env_cfg, - stack_ik_rel_blueprint_env_cfg, - stack_ik_rel_env_cfg, - stack_ik_rel_env_cfg_skillgen, - stack_ik_rel_instance_randomize_env_cfg, - stack_ik_rel_visuomotor_cosmos_env_cfg, - stack_ik_rel_visuomotor_env_cfg, - stack_joint_pos_env_cfg, - stack_joint_pos_instance_randomize_env_cfg, -) +from . import agents ## # Register Gym environments. @@ -31,7 +18,7 @@ id="Isaac-Stack-Cube-Franka-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_joint_pos_env_cfg.FrankaCubeStackEnvCfg, + "env_cfg_entry_point": f"{__name__}.stack_joint_pos_env_cfg:FrankaCubeStackEnvCfg", }, disable_env_checker=True, ) @@ -40,7 +27,9 @@ id="Isaac-Stack-Cube-Instance-Randomize-Franka-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_joint_pos_instance_randomize_env_cfg.FrankaCubeStackInstanceRandomizeEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.stack_joint_pos_instance_randomize_env_cfg:FrankaCubeStackInstanceRandomizeEnvCfg" + ), }, disable_env_checker=True, ) @@ -54,8 +43,8 @@ id="Isaac-Stack-Cube-Franka-IK-Rel-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_env_cfg.FrankaCubeStackEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + "env_cfg_entry_point": f"{__name__}.stack_ik_rel_env_cfg:FrankaCubeStackEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_low_dim.json", }, disable_env_checker=True, ) @@ -64,8 +53,8 @@ id="Isaac-Stack-Cube-Franka-IK-Rel-Visuomotor-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_visuomotor_env_cfg.FrankaCubeStackVisuomotorEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_image_84.json"), + "env_cfg_entry_point": f"{__name__}.stack_ik_rel_visuomotor_env_cfg:FrankaCubeStackVisuomotorEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_image_200.json", }, disable_env_checker=True, ) @@ -74,8 +63,10 @@ id="Isaac-Stack-Cube-Franka-IK-Rel-Visuomotor-Cosmos-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_visuomotor_cosmos_env_cfg.FrankaCubeStackVisuomotorCosmosEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_image_cosmos.json"), + "env_cfg_entry_point": ( + f"{__name__}.stack_ik_rel_visuomotor_cosmos_env_cfg:FrankaCubeStackVisuomotorCosmosEnvCfg" + ), + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_image_cosmos.json", }, disable_env_checker=True, ) @@ -84,8 +75,8 @@ id="Isaac-Stack-Cube-Franka-IK-Abs-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_abs_env_cfg.FrankaCubeStackEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + "env_cfg_entry_point": f"{__name__}.stack_ik_abs_env_cfg:FrankaCubeStackEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_low_dim.json", }, disable_env_checker=True, ) @@ -94,7 +85,9 @@ id="Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_instance_randomize_env_cfg.FrankaCubeStackInstanceRandomizeEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.stack_ik_rel_instance_randomize_env_cfg:FrankaCubeStackInstanceRandomizeEnvCfg" + ), }, disable_env_checker=True, ) @@ -103,7 +96,7 @@ id="Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_blueprint_env_cfg.FrankaCubeStackBlueprintEnvCfg, + "env_cfg_entry_point": f"{__name__}.stack_ik_rel_blueprint_env_cfg:FrankaCubeStackBlueprintEnvCfg", }, disable_env_checker=True, ) @@ -112,8 +105,8 @@ id="Isaac-Stack-Cube-Franka-IK-Rel-Skillgen-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_env_cfg_skillgen.FrankaCubeStackSkillgenEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + "env_cfg_entry_point": f"{__name__}.stack_ik_rel_env_cfg_skillgen:FrankaCubeStackSkillgenEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_low_dim.json", }, disable_env_checker=True, ) @@ -122,8 +115,8 @@ id="Isaac-Stack-Cube-Bin-Franka-IK-Rel-Mimic-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": bin_stack_ik_rel_env_cfg.FrankaBinStackEnvCfg, - "robomimic_bc_cfg_entry_point": os.path.join(agents.__path__[0], "robomimic/bc_rnn_low_dim.json"), + "env_cfg_entry_point": f"{__name__}.bin_stack_ik_rel_env_cfg:FrankaBinStackEnvCfg", + "robomimic_bc_cfg_entry_point": f"{agents.__name__}:robomimic/bc_rnn_low_dim.json", }, disable_env_checker=True, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_image_84.json b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_image_200.json similarity index 98% rename from source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_image_84.json rename to source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_image_200.json index 94e722fd0b11..33117b90e3f3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_image_84.json +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/agents/robomimic/bc_rnn_image_200.json @@ -196,8 +196,8 @@ }, "obs_randomizer_class": "CropRandomizer", "obs_randomizer_kwargs": { - "crop_height": 76, - "crop_width": 76, + "crop_height": 181, + "crop_width": 181, "num_crops": 1, "pos_enc": false } diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_ik_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_ik_rel_env_cfg.py index fd4b386249e6..91ddcbb851cc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_ik_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_ik_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py index 2952593df86a..4121c1bb2e20 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_abs_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_abs_env_cfg.py index 17dbe0ce2a72..8d9b18bcd95f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_abs_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_abs_env_cfg.py @@ -1,11 +1,11 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg -from isaaclab.devices.device_base import DevicesCfg -from isaaclab.devices.openxr.openxr_device import OpenXRDevice, OpenXRDeviceCfg +from isaaclab.devices.device_base import DeviceBase, DevicesCfg +from isaaclab.devices.openxr.openxr_device import OpenXRDeviceCfg from isaaclab.devices.openxr.retargeters.manipulator.gripper_retargeter import GripperRetargeterCfg from isaaclab.devices.openxr.retargeters.manipulator.se3_abs_retargeter import Se3AbsRetargeterCfg from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg @@ -42,14 +42,14 @@ def __post_init__(self): "handtracking": OpenXRDeviceCfg( retargeters=[ Se3AbsRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, zero_out_xy_rotation=True, use_wrist_rotation=False, use_wrist_position=True, sim_device=self.sim.device, ), GripperRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device ), ], sim_device=self.sim.device, diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_blueprint_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_blueprint_env_cfg.py index 72ffa93a5ab4..3586508df2dc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_blueprint_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_blueprint_env_cfg.py @@ -1,9 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 os + import torch from torchvision.utils import save_image diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg.py index f173ee644cef..16eb9b9f087e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg.py @@ -1,12 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg -from isaaclab.devices.device_base import DevicesCfg +from isaaclab.devices.device_base import DeviceBase, DevicesCfg from isaaclab.devices.keyboard import Se3KeyboardCfg -from isaaclab.devices.openxr.openxr_device import OpenXRDevice, OpenXRDeviceCfg +from isaaclab.devices.openxr.openxr_device import OpenXRDeviceCfg from isaaclab.devices.openxr.retargeters.manipulator.gripper_retargeter import GripperRetargeterCfg from isaaclab.devices.openxr.retargeters.manipulator.se3_rel_retargeter import Se3RelRetargeterCfg from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg @@ -45,7 +45,7 @@ def __post_init__(self): "handtracking": OpenXRDeviceCfg( retargeters=[ Se3RelRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, zero_out_xy_rotation=True, use_wrist_rotation=False, use_wrist_position=True, @@ -54,7 +54,7 @@ def __post_init__(self): sim_device=self.sim.device, ), GripperRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device ), ], sim_device=self.sim.device, diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg_skillgen.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg_skillgen.py index b95640be8a7b..d2a2bd62100a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg_skillgen.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_env_cfg_skillgen.py @@ -1,12 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg -from isaaclab.devices.device_base import DevicesCfg +from isaaclab.devices.device_base import DeviceBase, DevicesCfg from isaaclab.devices.keyboard import Se3KeyboardCfg -from isaaclab.devices.openxr.openxr_device import OpenXRDevice, OpenXRDeviceCfg +from isaaclab.devices.openxr.openxr_device import OpenXRDeviceCfg from isaaclab.devices.openxr.retargeters.manipulator.gripper_retargeter import GripperRetargeterCfg from isaaclab.devices.openxr.retargeters.manipulator.se3_rel_retargeter import Se3RelRetargeterCfg from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg @@ -129,7 +129,7 @@ def __post_init__(self): "handtracking": OpenXRDeviceCfg( retargeters=[ Se3RelRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, zero_out_xy_rotation=True, use_wrist_rotation=False, use_wrist_position=True, @@ -138,7 +138,7 @@ def __post_init__(self): sim_device=self.sim.device, ), GripperRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device ), ], sim_device=self.sim.device, diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_instance_randomize_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_instance_randomize_env_cfg.py index cee2530ee4f7..4f31184585dd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_instance_randomize_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_instance_randomize_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_cosmos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_cosmos_env_cfg.py index e5b181abaefd..2cdd8ef39ee4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_cosmos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_cosmos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2025-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 @@ -153,8 +153,8 @@ def __post_init__(self): ) # Set settings for camera rendering - self.rerender_on_reset = True - self.sim.render.antialiasing_mode = "OFF" # disable dlss + self.num_rerenders_on_reset = 1 + self.sim.render.antialiasing_mode = "OFF" # List of image observations in policy observations self.image_obs_list = ["table_cam", "wrist_cam"] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_env_cfg.py index 7f990c5fd3ae..13040620b788 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_ik_rel_visuomotor_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -204,8 +204,8 @@ def __post_init__(self): self.scene.wrist_cam = CameraCfg( prim_path="{ENV_REGEX_NS}/Robot/panda_hand/wrist_cam", update_period=0.0, - height=84, - width=84, + height=200, + width=200, data_types=["rgb", "distance_to_image_plane"], spawn=sim_utils.PinholeCameraCfg( focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 2) @@ -219,8 +219,8 @@ def __post_init__(self): self.scene.table_cam = CameraCfg( prim_path="{ENV_REGEX_NS}/table_cam", update_period=0.0, - height=84, - width=84, + height=200, + width=200, data_types=["rgb", "distance_to_image_plane"], spawn=sim_utils.PinholeCameraCfg( focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 2) @@ -231,8 +231,8 @@ def __post_init__(self): ) # Set settings for camera rendering - self.rerender_on_reset = True - self.sim.render.antialiasing_mode = "OFF" # disable dlss + self.num_rerenders_on_reset = 3 + self.sim.render.antialiasing_mode = "DLAA" # Use DLAA for higher quality rendering # List of image observations in policy observations self.image_obs_list = ["table_cam", "wrist_cam"] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py index ae01d277ba5d..e344f0021db2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,6 +59,7 @@ class EventCfg: @configclass class FrankaCubeStackEnvCfg(StackEnvCfg): + """Configuration for the Franka Cube Stack Environment.""" def __post_init__(self): # post init of parent diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py index 5ac1e9e2d2b9..3e14199a2630 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py index 7aa2ebad0fce..760669cca629 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_joint_pos_env_cfg.py index cdf9baeb4e5b..4506a95eaba9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,7 +6,8 @@ from isaaclab.assets import RigidObjectCfg, SurfaceGripperCfg from isaaclab.devices import DevicesCfg -from isaaclab.devices.openxr import OpenXRDevice, OpenXRDeviceCfg +from isaaclab.devices.device_base import DeviceBase +from isaaclab.devices.openxr.openxr_device import OpenXRDeviceCfg from isaaclab.devices.openxr.retargeters import GripperRetargeterCfg, Se3AbsRetargeterCfg from isaaclab.envs.mdp.actions.actions_cfg import SurfaceGripperBinaryActionCfg from isaaclab.managers import EventTermCfg as EventTerm @@ -55,6 +56,7 @@ class EventCfg: @configclass class ObservationGalbotLeftArmGripperCfg: + """Observations for the Galbot Left Arm Gripper.""" @configclass class PolicyCfg(ObsGroup): @@ -155,7 +157,6 @@ def __post_init__(self): @configclass class GalbotLeftArmCubeStackEnvCfg(StackEnvCfg): - def __post_init__(self): # post init of parent super().__post_init__() @@ -252,14 +253,14 @@ def __post_init__(self): "handtracking": OpenXRDeviceCfg( retargeters=[ Se3AbsRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_LEFT, + bound_hand=DeviceBase.TrackingTarget.HAND_LEFT, zero_out_xy_rotation=True, use_wrist_rotation=False, use_wrist_position=True, sim_device=self.sim.device, ), GripperRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_LEFT, sim_device=self.sim.device + bound_hand=DeviceBase.TrackingTarget.HAND_LEFT, sim_device=self.sim.device ), ], sim_device=self.sim.device, @@ -271,16 +272,13 @@ def __post_init__(self): @configclass class GalbotRightArmCubeStackEnvCfg(GalbotLeftArmCubeStackEnvCfg): - def __post_init__(self): # post init of parent super().__post_init__() - l, r = self.events.randomize_cube_positions.params["pose_range"]["y"] - self.events.randomize_cube_positions.params["pose_range"]["y"] = ( - -r, - -l, - ) # move to area below right hand + # Move to area below right hand (invert y-axis) + left, right = self.events.randomize_cube_positions.params["pose_range"]["y"] + self.events.randomize_cube_positions.params["pose_range"]["y"] = (-right, -left) # Set actions for the specific robot type (galbot) self.actions.arm_action = mdp.JointPositionActionCfg( @@ -310,14 +308,14 @@ def __post_init__(self): "handtracking": OpenXRDeviceCfg( retargeters=[ Se3AbsRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, zero_out_xy_rotation=True, use_wrist_rotation=False, use_wrist_position=True, sim_device=self.sim.device, ), GripperRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device ), ], sim_device=self.sim.device, diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_rmp_rel_env_cfg.py index 8eb970dc3e55..eebb79e1d315 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/stack_rmp_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +7,9 @@ import os import isaaclab.sim as sim_utils -from isaaclab.devices.device_base import DevicesCfg +from isaaclab.devices.device_base import DeviceBase, DevicesCfg from isaaclab.devices.keyboard import Se3KeyboardCfg -from isaaclab.devices.openxr import OpenXRDevice, OpenXRDeviceCfg +from isaaclab.devices.openxr.openxr_device import OpenXRDeviceCfg from isaaclab.devices.openxr.retargeters import GripperRetargeterCfg, Se3RelRetargeterCfg from isaaclab.devices.spacemouse import Se3SpaceMouseCfg from isaaclab.envs.mdp.actions.rmpflow_actions_cfg import RMPFlowActionCfg @@ -36,6 +36,7 @@ ## @configclass class RmpFlowGalbotLeftArmCubeStackEnvCfg(stack_joint_pos_env_cfg.GalbotLeftArmCubeStackEnvCfg): + """Configuration for the Galbot Left Arm Cube Stack Environment.""" def __post_init__(self): # post init of parent @@ -80,7 +81,7 @@ def __post_init__(self): "handtracking": OpenXRDeviceCfg( retargeters=[ Se3RelRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_LEFT, + bound_hand=DeviceBase.TrackingTarget.HAND_LEFT, zero_out_xy_rotation=True, use_wrist_rotation=False, use_wrist_position=True, @@ -89,7 +90,7 @@ def __post_init__(self): sim_device=self.sim.device, ), GripperRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_LEFT, sim_device=self.sim.device + bound_hand=DeviceBase.TrackingTarget.HAND_LEFT, sim_device=self.sim.device ), ], sim_device=self.sim.device, @@ -104,7 +105,6 @@ def __post_init__(self): ## @configclass class RmpFlowGalbotRightArmCubeStackEnvCfg(stack_joint_pos_env_cfg.GalbotRightArmCubeStackEnvCfg): - def __post_init__(self): # post init of parent super().__post_init__() @@ -150,7 +150,7 @@ def __post_init__(self): "handtracking": OpenXRDeviceCfg( retargeters=[ Se3RelRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, zero_out_xy_rotation=True, use_wrist_rotation=False, use_wrist_position=True, @@ -159,7 +159,7 @@ def __post_init__(self): sim_device=self.sim.device, ), GripperRetargeterCfg( - bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device + bound_hand=DeviceBase.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device ), ], sim_device=self.sim.device, @@ -174,7 +174,6 @@ def __post_init__(self): ## @configclass class RmpFlowGalbotLeftArmCubeStackVisuomotorEnvCfg(RmpFlowGalbotLeftArmCubeStackEnvCfg): - def __post_init__(self): # post init of parent super().__post_init__() @@ -271,8 +270,8 @@ def __post_init__(self): ) # Set settings for camera rendering - self.rerender_on_reset = True - self.sim.render.antialiasing_mode = "OFF" # disable dlss + self.num_rerenders_on_reset = 3 + self.sim.render.antialiasing_mode = "DLAA" # Use DLAA for higher quality rendering # List of image observations in policy observations self.image_obs_list = ["ego_cam", "left_wrist_cam", "right_wrist_cam"] @@ -299,8 +298,10 @@ def __post_init__(self): joint_names=["left_gripper_.*_joint"], open_command_expr={"left_gripper_.*_joint": 0.035}, close_command_expr={"left_gripper_.*_joint": 0.023}, - # real gripper close data is 0.0235, close to it to meet data distribution, but smaller to ensure robust grasping. - # during VLA inference, we set the close command to '0.023' since the VLA has never seen the gripper fully closed. + # real gripper close data is 0.0235, close to it to meet data distribution, + # but smaller to ensure robust grasping. + # during VLA inference, we set the close command to '0.023' since the VLA + # has never seen the gripper fully closed. ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py index 41887a8df8b2..81165e4a28a4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_ik_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_ik_rel_env_cfg.py index 00c379ef19fa..0e6b2df08b29 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_ik_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_ik_rel_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -16,6 +16,7 @@ @configclass class UR10LongSuctionCubeStackEnvCfg(stack_joint_pos_env_cfg.UR10LongSuctionCubeStackEnvCfg): + """Configuration for the UR10 Long Suction Cube Stack Environment.""" def __post_init__(self): # post init of parent @@ -49,7 +50,6 @@ def __post_init__(self): @configclass class UR10ShortSuctionCubeStackEnvCfg(stack_joint_pos_env_cfg.UR10ShortSuctionCubeStackEnvCfg): - def __post_init__(self): # post init of parent super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_joint_pos_env_cfg.py index 726d90794722..c3d73a3fb3c9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/stack_joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -135,6 +135,7 @@ def __post_init__(self): @configclass class UR10LongSuctionCubeStackEnvCfg(UR10CubeStackEnvCfg): + """Configuration for the UR10 Long Suction Cube Stack Environment.""" def __post_init__(self): # post init of parent @@ -176,7 +177,6 @@ def __post_init__(self): @configclass class UR10ShortSuctionCubeStackEnvCfg(UR10CubeStackEnvCfg): - def __post_init__(self): # post init of parent super().__post_init__() diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/__init__.py index 8298b94d9b91..ea04fcc468e9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/franka_stack_events.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/franka_stack_events.py index 009a44b1b372..3d9e1db48629 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/franka_stack_events.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/franka_stack_events.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +8,10 @@ import math import random -import torch from typing import TYPE_CHECKING +import torch + from isaacsim.core.utils.extensions import enable_extension import isaaclab.utils.math as math_utils diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/observations.py index 2f65cd916ee9..31123e71a308 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/observations.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING, Literal +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection from isaaclab.managers import SceneEntityCfg @@ -434,7 +435,7 @@ def cube_poses_in_base_frame( return pos_cubes_base elif return_key == "quat": return quat_cubes_base - elif return_key is None: + else: return torch.cat((pos_cubes_base, quat_cubes_base), dim=1) @@ -447,7 +448,8 @@ def object_abs_obs_in_base_frame( robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ): """ - Object Abs observations (in base frame): remove the relative observations, and add abs gripper pos and quat in robot base frame + Object Abs observations (in base frame): remove the relative observations, + and add abs gripper pos and quat in robot base frame cube_1 pos, cube_1 quat, cube_2 pos, @@ -528,5 +530,5 @@ def ee_frame_pose_in_base_frame( return ee_pos_in_base elif return_key == "quat": return ee_quat_in_base - elif return_key is None: + else: return torch.cat((ee_pos_in_base, ee_quat_in_base), dim=1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/terminations.py index e306f9eb4a0a..2e4c14afcea1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +11,10 @@ from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation, RigidObject from isaaclab.managers import SceneEntityCfg diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_env_cfg.py index 55578cebaf47..5c772a11760e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_instance_randomize_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_instance_randomize_env_cfg.py index 8ef135c4a082..526297b95617 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_instance_randomize_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/stack_instance_randomize_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/__init__.py index 6afd5be0196e..f611f9e8eb4d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/__init__.py index 589136d47af9..99d0035ef264 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/__init__.py index 84a6dd920c3b..bcedc58f3f0a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/rsl_rl_ppo_cfg.py index 4b23def89b2f..93ec98732f8c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/rsl_rl_ppo_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml index 5473188cbd86..3eba30e7fc18 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py index 2f2162fdce9b..96b60705bb50 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/config/anymal_c/navigation_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/__init__.py index 0a2576ceb9f5..213391d362b1 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/pre_trained_policy_action.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/pre_trained_policy_action.py index 0437963698cc..c25558c78846 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/pre_trained_policy_action.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/pre_trained_policy_action.py @@ -1,14 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from dataclasses import MISSING from typing import TYPE_CHECKING +import torch + import isaaclab.utils.math as math_utils from isaaclab.assets import Articulation from isaaclab.managers import ActionTerm, ActionTermCfg, ObservationGroupCfg, ObservationManager diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/rewards.py index 59c7ec5a936f..ccaad755d087 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/navigation/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + if TYPE_CHECKING: from isaaclab.envs import ManagerBasedRLEnv diff --git a/source/isaaclab_tasks/isaaclab_tasks/utils/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/utils/__init__.py index 226167e62746..495b207c3194 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/utils/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/utils/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py b/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py index 6e2648aa0295..525b425917fa 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py +++ b/source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py @@ -1,11 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Sub-module with utilities for the hydra configuration system.""" - import functools from collections.abc import Callable diff --git a/source/isaaclab_tasks/isaaclab_tasks/utils/importer.py b/source/isaaclab_tasks/isaaclab_tasks/utils/importer.py index 075feb3c5279..ddbab7ede412 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/utils/importer.py +++ b/source/isaaclab_tasks/isaaclab_tasks/utils/importer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -60,13 +60,16 @@ def _walk_packages( ``pkgutil.walk_packages`` function for more details. """ + # Default blacklist if blacklist_pkgs is None: blacklist_pkgs = [] - def seen(p, m={}): + def seen(p: str, m: dict[str, bool] = {}) -> bool: + """Check if a package has been seen before.""" if p in m: return True - m[p] = True # noqa: R503 + m[p] = True + return False for info in pkgutil.iter_modules(path, prefix): # check blacklisted @@ -85,7 +88,7 @@ def seen(p, m={}): else: raise else: - path = getattr(sys.modules[info.name], "__path__", None) or [] + path: list = getattr(sys.modules[info.name], "__path__", []) # don't traverse path items we've seen before path = [p for p in path if not seen(p)] diff --git a/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py index b4f788a9bcbd..0002c5d58d9a 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/utils/parse_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,11 +6,12 @@ """Sub-module with utilities for parsing and loading configurations.""" import collections -import gymnasium as gym import importlib import inspect import os import re + +import gymnasium as gym import yaml from isaaclab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg diff --git a/source/isaaclab_tasks/setup.py b/source/isaaclab_tasks/setup.py index 38a1d1a6e02a..455c56689ce0 100644 --- a/source/isaaclab_tasks/setup.py +++ b/source/isaaclab_tasks/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,8 +6,8 @@ """Installation script for the 'isaaclab_tasks' python package.""" import os -import toml +import toml from setuptools import setup # Obtain the extension data from the extension.toml file diff --git a/source/isaaclab_tasks/test/benchmarking/configs.yaml b/source/isaaclab_tasks/test/benchmarking/configs.yaml index b4bc4fc043f6..d5df21551b73 100644 --- a/source/isaaclab_tasks/test/benchmarking/configs.yaml +++ b/source/isaaclab_tasks/test/benchmarking/configs.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/test/benchmarking/conftest.py b/source/isaaclab_tasks/test/benchmarking/conftest.py index c878d7e88238..6a13b1898a52 100644 --- a/source/isaaclab_tasks/test/benchmarking/conftest.py +++ b/source/isaaclab_tasks/test/benchmarking/conftest.py @@ -1,13 +1,15 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import json -import env_benchmark_test_utils as utils import pytest +# Local imports should be imported last +import env_benchmark_test_utils as utils # isort: skip + # Global variable for storing KPI data GLOBAL_KPI_STORE = {} diff --git a/source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py b/source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py index 0c939ca01660..52dbeadda3a0 100644 --- a/source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py +++ b/source/isaaclab_tasks/test/benchmarking/env_benchmark_test_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,15 +6,16 @@ import glob import json import math -import numpy as np import os import re -import yaml from datetime import datetime -import carb +import numpy as np +import yaml from tensorboard.backend.event_processing import event_accumulator +import carb + def get_env_configs(configs_path): """Get environment configurations from yaml filepath.""" @@ -150,9 +151,9 @@ def _retrieve_logs(workflow, task): """Retrieve training logs.""" # first grab all log files repo_path = os.path.join(carb.tokens.get_tokens_interface().resolve("${app}"), "..") - from isaacsim.core.version import get_version + from isaaclab.utils.version import get_isaac_sim_version - if int(get_version()[2]) < 5: + if get_isaac_sim_version().major < 5: repo_path = os.path.join(repo_path, "..") if workflow == "rl_games": log_files_path = os.path.join(repo_path, f"logs/{workflow}/{task}/*/summaries/*") diff --git a/source/isaaclab_tasks/test/benchmarking/test_environments_training.py b/source/isaaclab_tasks/test/benchmarking/test_environments_training.py index 5fae937ef84d..70fd562089a1 100644 --- a/source/isaaclab_tasks/test/benchmarking/test_environments_training.py +++ b/source/isaaclab_tasks/test/benchmarking/test_environments_training.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,17 +11,18 @@ app_launcher = AppLauncher(headless=True, enable_cameras=True) simulation_app = app_launcher.app -import gymnasium as gym import os import subprocess import sys import time -import carb import env_benchmark_test_utils as utils +import gymnasium as gym import pytest -from isaaclab.utils.pretrained_checkpoint import WORKFLOW_EXPERIMENT_NAME_VARIABLE, WORKFLOW_TRAINER +import carb + +from isaaclab_rl.utils.pretrained_checkpoint import WORKFLOW_EXPERIMENT_NAME_VARIABLE, WORKFLOW_TRAINER def setup_environment(): diff --git a/source/isaaclab_tasks/test/env_test_utils.py b/source/isaaclab_tasks/test/env_test_utils.py index 1034fd9ac923..b6f0383abee1 100644 --- a/source/isaaclab_tasks/test/env_test_utils.py +++ b/source/isaaclab_tasks/test/env_test_utils.py @@ -1,21 +1,22 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 """Shared test utilities for Isaac Lab environments.""" -import gymnasium as gym import inspect import os + +import gymnasium as gym +import pytest import torch import carb import omni.usd -import pytest -from isaacsim.core.version import get_version from isaaclab.envs.utils.spaces import sample_space +from isaaclab.utils.version import get_isaac_sim_version from isaaclab_tasks.utils.parse_cfg import parse_env_cfg @@ -110,8 +111,7 @@ def _run_environments( """ # skip test if stage in memory is not supported - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5 and create_stage_in_memory: + if get_isaac_sim_version().major < 5 and create_stage_in_memory: pytest.skip("Stage in memory is not supported in this version of Isaac Sim") # skip suction gripper environments as they require CPU simulation and cannot be run with GPU simulation diff --git a/source/isaaclab_tasks/test/test_environment_determinism.py b/source/isaaclab_tasks/test/test_environment_determinism.py index 016e60cb2f61..52ac1f34980e 100644 --- a/source/isaaclab_tasks/test/test_environment_determinism.py +++ b/source/isaaclab_tasks/test/test_environment_determinism.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,11 +15,11 @@ """Rest everything follows.""" import gymnasium as gym +import pytest import torch import carb import omni.usd -import pytest import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.parse_cfg import parse_env_cfg diff --git a/source/isaaclab_tasks/test/test_environments.py b/source/isaaclab_tasks/test/test_environments.py index 2a0c9d4ea525..879948f9d9a8 100644 --- a/source/isaaclab_tasks/test/test_environments.py +++ b/source/isaaclab_tasks/test/test_environments.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +7,8 @@ import sys -# Import pinocchio in the main script to force the use of the dependencies installed by IsaacLab and not the one installed by Isaac Sim +# Import pinocchio in the main script to force the use of the dependencies +# installed by IsaacLab and not the one installed by Isaac Sim. # pinocchio is required by the Pink IK controller if sys.platform != "win32": import pinocchio # noqa: F401 @@ -22,10 +23,12 @@ """Rest everything follows.""" import pytest -from env_test_utils import _run_environments, setup_environment import isaaclab_tasks # noqa: F401 +# Local imports should be imported last +from env_test_utils import _run_environments, setup_environment # isort: skip + @pytest.mark.parametrize("num_envs, device", [(32, "cuda"), (1, "cuda")]) @pytest.mark.parametrize("task_name", setup_environment(include_play=False, factory_envs=False, multi_agent=False)) diff --git a/source/isaaclab_tasks/test/test_environments_with_stage_in_memory.py b/source/isaaclab_tasks/test/test_environments_with_stage_in_memory.py index 70dcf94961f3..8a99436b91c7 100644 --- a/source/isaaclab_tasks/test/test_environments_with_stage_in_memory.py +++ b/source/isaaclab_tasks/test/test_environments_with_stage_in_memory.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +7,8 @@ import sys -# Import pinocchio in the main script to force the use of the dependencies installed by IsaacLab and not the one installed by Isaac Sim +# Import pinocchio in the main script to force the use of the dependencies +# installed by IsaacLab and not the one installed by Isaac Sim. # pinocchio is required by the Pink IK controller if sys.platform != "win32": import pinocchio # noqa: F401 @@ -18,15 +19,18 @@ app_launcher = AppLauncher(headless=True, enable_cameras=True) simulation_app = app_launcher.app -from isaacsim.core.version import get_version +from isaaclab.utils.version import get_isaac_sim_version """Rest everything follows.""" import pytest -from env_test_utils import _run_environments, setup_environment import isaaclab_tasks # noqa: F401 +# Local imports should be imported last +from env_test_utils import _run_environments, setup_environment # isort: skip + + # note, running an env test without stage in memory then # running an env test with stage in memory causes IsaacLab to hang. # so, here we run all envs with stage in memory separately @@ -36,8 +40,7 @@ # @pytest.mark.parametrize("task_name", setup_environment(include_play=False,factory_envs=False, multi_agent=False)) # def test_environments_with_stage_in_memory_and_clone_in_fabric_disabled(task_name, num_envs, device): # # skip test if stage in memory is not supported -# isaac_sim_version = float(".".join(get_version()[2])) -# if isaac_sim_version < 5: +# if get_isaac_sim_version().major < 5: # pytest.skip("Stage in memory is not supported in this version of Isaac Sim") # # run environments with stage in memory @@ -48,8 +51,7 @@ @pytest.mark.parametrize("task_name", setup_environment(include_play=False, factory_envs=False, multi_agent=False)) def test_environments_with_stage_in_memory_and_clone_in_fabric_disabled(task_name, num_envs, device): # skip test if stage in memory is not supported - isaac_sim_version = float(".".join(get_version()[2])) - if isaac_sim_version < 5: + if get_isaac_sim_version().major < 5: pytest.skip("Stage in memory is not supported in this version of Isaac Sim") # run environments with stage in memory diff --git a/source/isaaclab_tasks/test/test_factory_environments.py b/source/isaaclab_tasks/test/test_factory_environments.py index 7b445a453f1f..059080006557 100644 --- a/source/isaaclab_tasks/test/test_factory_environments.py +++ b/source/isaaclab_tasks/test/test_factory_environments.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -14,10 +14,12 @@ """Rest everything follows.""" import pytest -from env_test_utils import _check_random_actions, setup_environment import isaaclab_tasks # noqa: F401 +# Local imports should be imported last +from env_test_utils import _check_random_actions, setup_environment # isort: skip + @pytest.mark.parametrize("num_envs, device", [(32, "cuda"), (1, "cuda")]) @pytest.mark.parametrize("task_name", setup_environment(factory_envs=True, multi_agent=False)) diff --git a/source/isaaclab_tasks/test/test_hydra.py b/source/isaaclab_tasks/test/test_hydra.py index c3b24fcaf8d8..5c81cb3e650f 100644 --- a/source/isaaclab_tasks/test/test_hydra.py +++ b/source/isaaclab_tasks/test/test_hydra.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/source/isaaclab_tasks/test/test_lift_teddy_bear.py b/source/isaaclab_tasks/test/test_lift_teddy_bear.py index 9a7eaad40cf7..e131e0357498 100644 --- a/source/isaaclab_tasks/test/test_lift_teddy_bear.py +++ b/source/isaaclab_tasks/test/test_lift_teddy_bear.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,7 +7,8 @@ import sys -# Import pinocchio in the main script to force the use of the dependencies installed by IsaacLab and not the one installed by Isaac Sim +# Import pinocchio in the main script to force the use of the dependencies +# installed by IsaacLab and not the one installed by Isaac Sim. # pinocchio is required by the Pink IK controller if sys.platform != "win32": import pinocchio # noqa: F401 @@ -23,10 +24,12 @@ """Rest everything follows.""" import pytest -from env_test_utils import _run_environments import isaaclab_tasks # noqa: F401 +# Local imports should be imported last +from env_test_utils import _run_environments # isort: skip + @pytest.mark.parametrize("num_envs, device", [(32, "cuda"), (1, "cuda")]) def test_lift_teddy_bear_environment(num_envs, device): diff --git a/source/isaaclab_tasks/test/test_multi_agent_environments.py b/source/isaaclab_tasks/test/test_multi_agent_environments.py index 21b3ac3b84d8..478cb3942e10 100644 --- a/source/isaaclab_tasks/test/test_multi_agent_environments.py +++ b/source/isaaclab_tasks/test/test_multi_agent_environments.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,10 +15,12 @@ """Rest everything follows.""" import pytest -from env_test_utils import _check_random_actions, setup_environment import isaaclab_tasks # noqa: F401 +# Local imports should be imported last +from env_test_utils import _check_random_actions, setup_environment # isort: skip + @pytest.mark.parametrize("num_envs, device", [(32, "cuda"), (1, "cuda")]) @pytest.mark.parametrize("task_name", setup_environment(multi_agent=True)) diff --git a/source/isaaclab_tasks/test/test_record_video.py b/source/isaaclab_tasks/test/test_record_video.py index 9258fd2119fa..a84eb846e887 100644 --- a/source/isaaclab_tasks/test/test_record_video.py +++ b/source/isaaclab_tasks/test/test_record_video.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,17 +13,20 @@ """Rest everything follows.""" -import gymnasium as gym import os + +import gymnasium as gym +import pytest import torch import omni.usd -import pytest -from env_test_utils import setup_environment import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils import parse_env_cfg +# Local imports should be imported last +from env_test_utils import setup_environment # isort: skip + @pytest.fixture(scope="function") def setup_video_params(): diff --git a/source/isaaclab_tasks/test/test_rl_device_separation.py b/source/isaaclab_tasks/test/test_rl_device_separation.py new file mode 100644 index 000000000000..ef6bd1e093f1 --- /dev/null +++ b/source/isaaclab_tasks/test/test_rl_device_separation.py @@ -0,0 +1,379 @@ +# 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 + +"""Test RL device separation across all supported RL libraries. + +This test verifies that RL library wrappers correctly handle device transfers when the +simulation device differs from the RL training device. + +Device Architecture: + 1. sim_device: Where physics simulation runs and environment buffers live + 2. rl_device: Where policy networks and training computations occur + +Test Scenarios: + - GPU simulation + GPU RL: Same device (no transfers needed, optimal performance) + - GPU simulation + CPU RL: Cross-device transfers (wrapper handles transfers) + - CPU simulation + CPU RL: CPU-only operation + +Each test verifies the wrapper correctly: + 1. Unwrapped env: operates entirely on sim_device + 2. Wrapper: accepts actions on rl_device (where policy generates them) + 3. Wrapper: internally transfers actions from rl_device → sim_device for env.step() + 4. Wrapper: transfers outputs from sim_device → rl_device (for policy to use) + +Tested Libraries: + - RSL-RL: TensorDict observations, device separation via OnPolicyRunner (agent_cfg.device) + * Wrapper returns data on sim_device, Runner handles transfers to rl_device + - RL Games: Dict observations, explicit rl_device parameter in wrapper + * Wrapper transfers data from sim_device to rl_device + - Stable-Baselines3: Numpy arrays (CPU-only by design) + * Wrapper converts tensors to/from numpy on CPU + - skrl: Dict observations, uses skrl.config.torch.device for RL device + * Wrapper keeps observations on sim_device, only transfers actions + +""" + +from isaaclab.app import AppLauncher + +# launch the simulator +app_launcher = AppLauncher(headless=True) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import gymnasium as gym +import pytest +import torch + +import carb +import omni.usd + +import isaaclab_tasks # noqa: F401 +from isaaclab_tasks.utils.parse_cfg import parse_env_cfg + +# Test environment - use Cartpole as it's simple and fast +TEST_ENV = "Isaac-Cartpole-v0" +NUM_ENVS = 4 + + +def _create_env(sim_device: str): + """Create and initialize a test environment. + + Args: + sim_device: Device for simulation (e.g., "cuda:0", "cpu") + + Returns: + Initialized gym environment + """ + # Create a new stage + omni.usd.get_context().new_stage() + # Reset the rtx sensors carb setting to False + carb.settings.get_settings().set_bool("/isaaclab/render/rtx_sensors", False) + + try: + env_cfg = parse_env_cfg(TEST_ENV, device=sim_device, num_envs=NUM_ENVS) + env = gym.make(TEST_ENV, cfg=env_cfg) + except Exception as e: + # Try to close environment on exception + if "env" in locals() and hasattr(env, "_is_closed"): + env.close() + else: + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): + e.obj.close() + pytest.fail(f"Failed to set-up the environment for task {TEST_ENV}. Error: {e}") + + # Disable control on stop + env.unwrapped.sim._app_control_on_stop_handle = None + return env + + +def _verify_unwrapped_env(env, sim_device: str): + """Verify unwrapped environment operates entirely on sim_device. + + Args: + env: Unwrapped gym environment + sim_device: Expected simulation device + """ + assert env.unwrapped.device == sim_device, ( + f"Environment device mismatch: expected {sim_device}, got {env.unwrapped.device}" + ) + + # Verify reset returns data on sim device + obs_dict, _ = env.reset() + for key, value in obs_dict.items(): + if isinstance(value, torch.Tensor): + assert value.device.type == torch.device(sim_device).type, ( + f"Unwrapped env obs '{key}' should be on {sim_device}, got {value.device}" + ) + + # Verify step returns data on sim device + action_space = env.unwrapped.single_action_space + test_action = torch.zeros(NUM_ENVS, action_space.shape[0], device=sim_device) + obs_dict, rew, term, trunc, extras = env.step(test_action) + assert rew.device.type == torch.device(sim_device).type, ( + f"Unwrapped env rewards should be on {sim_device}, got {rew.device}" + ) + assert term.device.type == torch.device(sim_device).type, ( + f"Unwrapped env terminated should be on {sim_device}, got {term.device}" + ) + + +def _verify_tensor_device(data, expected_device: str, name: str): + """Verify tensor or dict of tensors is on expected device. + + Args: + data: Tensor, dict of tensors, or numpy array + expected_device: Expected device string + name: Name for error messages + """ + if isinstance(data, torch.Tensor): + assert data.device.type == torch.device(expected_device).type, ( + f"{name} should be on {expected_device}, got {data.device}" + ) + elif isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, torch.Tensor): + assert value.device.type == torch.device(expected_device).type, ( + f"{name}['{key}'] should be on {expected_device}, got {value.device}" + ) + + +def _test_rsl_rl_device_separation(sim_device: str, rl_device: str): + """Helper function to test RSL-RL with specified device configuration. + + Note: RSL-RL device separation is handled by the OnPolicyRunner, not the wrapper. + The wrapper returns observations on sim_device, and the runner handles device transfers. + This test verifies the wrapper works correctly when actions come from a different device. + + Args: + sim_device: Device for simulation (e.g., "cuda:0", "cpu") + rl_device: Device for RL agent (e.g., "cuda:0", "cpu") - where policy generates actions + """ + from tensordict import TensorDict + + from isaaclab_rl.rsl_rl import RslRlVecEnvWrapper + + env = _create_env(sim_device) + _verify_unwrapped_env(env, sim_device) + + # Create wrapper - it uses sim_device, runner handles rl_device + env = RslRlVecEnvWrapper(env) + assert env.device == sim_device, f"Wrapper device should be {sim_device}" + + # Test reset - wrapper returns observations on sim_device + obs, extras = env.reset() + assert isinstance(obs, TensorDict), f"Expected TensorDict, got {type(obs)}" + _verify_tensor_device(obs, sim_device, "Observation") + + # Test step with action from RL device (simulating policy output) + # The wrapper should handle transferring action to sim_device internally + action = 2 * torch.rand(env.action_space.shape, device=rl_device) - 1 + obs, reward, dones, extras = env.step(action) + + # Verify outputs are on sim_device (runner would transfer to rl_device) + assert isinstance(obs, TensorDict), f"Expected TensorDict, got {type(obs)}" + _verify_tensor_device(obs, sim_device, "Step observation") + _verify_tensor_device(reward, sim_device, "Reward") + _verify_tensor_device(dones, sim_device, "Dones") + + env.close() + + +def _test_rl_games_device_separation(sim_device: str, rl_device: str): + """Helper function to test RL Games with specified device configuration. + + Args: + sim_device: Device for simulation (e.g., "cuda:0", "cpu") + rl_device: Device for RL agent (e.g., "cuda:0", "cpu") + """ + from isaaclab_rl.rl_games import RlGamesVecEnvWrapper + + env = _create_env(sim_device) + _verify_unwrapped_env(env, sim_device) + + # Create wrapper + env = RlGamesVecEnvWrapper(env, rl_device=rl_device, clip_obs=10.0, clip_actions=1.0) + + # Test reset + obs = env.reset() + _verify_tensor_device(obs, rl_device, "Observation") + + # Test step with action on RL device + action = 2 * torch.rand(NUM_ENVS, *env.action_space.shape, device=rl_device) - 1 + obs, reward, dones, info = env.step(action) + + # Verify outputs are on RL device + _verify_tensor_device(obs, rl_device, "Observation") + _verify_tensor_device(reward, rl_device, "Reward") + _verify_tensor_device(dones, rl_device, "Dones") + + env.close() + + +def _test_sb3_device_separation(sim_device: str): + """Helper function to test Stable-Baselines3 with specified device configuration. + + Note: SB3 always converts to CPU/numpy, so we don't test rl_device parameter. + + Args: + sim_device: Device for simulation (e.g., "cuda:0", "cpu") + """ + import numpy as np + + from isaaclab_rl.sb3 import Sb3VecEnvWrapper + + env = _create_env(sim_device) + _verify_unwrapped_env(env, sim_device) + + # Create wrapper + env = Sb3VecEnvWrapper(env) + + # Test reset - SB3 should return numpy arrays + obs = env.reset() + assert isinstance(obs, np.ndarray), f"SB3 observations should be numpy arrays, got {type(obs)}" + + # Test step with numpy action + action = 2 * np.random.rand(env.num_envs, *env.action_space.shape) - 1 + obs, reward, done, info = env.step(action) + + # Verify outputs are numpy arrays + assert isinstance(obs, np.ndarray), f"Observations should be numpy arrays, got {type(obs)}" + assert isinstance(reward, np.ndarray), f"Rewards should be numpy arrays, got {type(reward)}" + assert isinstance(done, np.ndarray), f"Dones should be numpy arrays, got {type(done)}" + + env.close() + + +def _test_skrl_device_separation(sim_device: str, rl_device: str): + """Helper function to test skrl with specified device configuration. + + Note: skrl uses skrl.config.torch.device for device configuration. + Observations remain on sim_device; only actions are transferred from rl_device. + + Args: + sim_device: Device for simulation (e.g., "cuda:0", "cpu") + rl_device: Device for RL agent (e.g., "cuda:0", "cpu") + """ + try: + import skrl + from skrl.envs.wrappers.torch import wrap_env + except ImportError: + pytest.skip("skrl not installed") + + # Configure skrl device + skrl.config.torch.device = torch.device(rl_device) + + env = _create_env(sim_device) + _verify_unwrapped_env(env, sim_device) + + # Wrap with skrl + env = wrap_env(env, wrapper="isaaclab") + + # Test reset + obs, info = env.reset() + assert isinstance(obs, (dict, torch.Tensor)), f"Observations should be dict or tensor, got {type(obs)}" + + # Test step with action on RL device + action = 2 * torch.rand(NUM_ENVS, *env.action_space.shape, device=skrl.config.torch.device) - 1 + transition = env.step(action) + + # Verify outputs - skrl keeps them on sim_device + if len(transition) == 5: + obs, reward, terminated, truncated, info = transition + _verify_tensor_device(obs, sim_device, "Observation") + _verify_tensor_device(reward, sim_device, "Reward") + _verify_tensor_device(terminated, sim_device, "Terminated") + _verify_tensor_device(truncated, sim_device, "Truncated") + elif len(transition) == 4: + obs, reward, done, info = transition + _verify_tensor_device(obs, sim_device, "Observation") + _verify_tensor_device(reward, sim_device, "Reward") + _verify_tensor_device(done, sim_device, "Done") + else: + pytest.fail(f"Unexpected number of return values from step: {len(transition)}") + + env.close() + + +# ============================================================================ +# Test Functions +# ============================================================================ + + +def test_rsl_rl_device_separation_gpu_to_gpu(): + """Test RSL-RL with GPU simulation and GPU RL (default configuration).""" + try: + import isaaclab_rl.rsl_rl # noqa: F401 + except ImportError: + pytest.skip("RSL-RL not installed") + + _test_rsl_rl_device_separation(sim_device="cuda:0", rl_device="cuda:0") + + +def test_rsl_rl_device_separation_gpu_to_cpu(): + """Test RSL-RL with GPU simulation and CPU RL (cross-device transfer).""" + try: + import isaaclab_rl.rsl_rl # noqa: F401 + except ImportError: + pytest.skip("RSL-RL not installed") + + _test_rsl_rl_device_separation(sim_device="cuda:0", rl_device="cpu") + + +def test_rl_games_device_separation_gpu_to_gpu(): + """Test RL Games with GPU simulation and GPU RL (default configuration).""" + try: + import isaaclab_rl.rl_games # noqa: F401 + except ImportError: + pytest.skip("RL Games not installed") + + _test_rl_games_device_separation(sim_device="cuda:0", rl_device="cuda:0") + + +def test_rl_games_device_separation_gpu_to_cpu(): + """Test RL Games with GPU simulation and CPU RL (cross-device transfer).""" + try: + import isaaclab_rl.rl_games # noqa: F401 + except ImportError: + pytest.skip("RL Games not installed") + + _test_rl_games_device_separation(sim_device="cuda:0", rl_device="cpu") + + +def test_sb3_device_separation_gpu(): + """Test Stable-Baselines3 with GPU simulation. + + Note: SB3 always converts to CPU/numpy, so only GPU simulation is tested. + """ + try: + import isaaclab_rl.sb3 # noqa: F401 + except ImportError: + pytest.skip("Stable-Baselines3 not installed") + + _test_sb3_device_separation(sim_device="cuda:0") + + +def test_skrl_device_separation_gpu(): + """Test skrl with GPU simulation and GPU policy (matching devices).""" + try: + import skrl # noqa: F401 + except ImportError: + pytest.skip("skrl not installed") + + _test_skrl_device_separation(sim_device="cuda:0", rl_device="cuda:0") + + +def test_skrl_device_separation_cpu_to_gpu(): + """Test skrl with CPU simulation and GPU policy. + + Note: Uses skrl.config.torch.device to set the policy device to GPU + while the environment runs on CPU. + """ + try: + import skrl # noqa: F401 + except ImportError: + pytest.skip("skrl not installed") + + _test_skrl_device_separation(sim_device="cpu", rl_device="cuda:0") diff --git a/tools/conftest.py b/tools/conftest.py index ed5db4cb69f4..a61c94f2c474 100644 --- a/tools/conftest.py +++ b/tools/conftest.py @@ -1,24 +1,21 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 contextlib import os - -# Platform-specific imports for real-time output streaming import select import subprocess import sys import time -# Third-party imports -from prettytable import PrettyTable - import pytest from junitparser import Error, JUnitXml, TestCase, TestSuite +from prettytable import PrettyTable -import tools.test_settings as test_settings +# Local imports +import test_settings as test_settings # isort: skip def pytest_ignore_collect(collection_path, config): @@ -144,20 +141,16 @@ def run_individual_tests(test_files, workspace_root, isaacsim_ci): env = os.environ.copy() # Determine timeout for this test - timeout = ( - test_settings.PER_TEST_TIMEOUTS[file_name] - if file_name in test_settings.PER_TEST_TIMEOUTS - else test_settings.DEFAULT_TIMEOUT - ) + timeout = test_settings.PER_TEST_TIMEOUTS.get(file_name, test_settings.DEFAULT_TIMEOUT) # Prepare command + # Note: Command options matter as they are used for cleanups inside AppLauncher cmd = [ sys.executable, "-m", "pytest", "--no-header", - "-c", - f"{workspace_root}/pytest.ini", + f"--config-file={workspace_root}/pyproject.toml", f"--junitxml=tests/test-reports-{str(file_name)}.xml", "--tb=short", ] @@ -409,12 +402,14 @@ def pytest_sessionstart(session): - test_status[test_path]["errors"] - test_status[test_path]["skipped"] ) - per_test_result_table.add_row([ - test_path, - test_status[test_path]["result"], - f"{test_status[test_path]['time_elapsed']:0.2f}", - f"{num_tests_passed}/{test_status[test_path]['tests']}", - ]) + per_test_result_table.add_row( + [ + test_path, + test_status[test_path]["result"], + f"{test_status[test_path]['time_elapsed']:0.2f}", + f"{num_tests_passed}/{test_status[test_path]['tests']}", + ] + ) summary_str += per_test_result_table.get_string() diff --git a/tools/install_deps.py b/tools/install_deps.py index 82474e3f3f3d..d2f08bb52e7c 100644 --- a/tools/install_deps.py +++ b/tools/install_deps.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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,9 +30,10 @@ import argparse import os import shutil -import toml from subprocess import PIPE, STDOUT, Popen +import toml + # add argparse arguments parser = argparse.ArgumentParser(description="A utility to install dependencies based on extension.toml files.") parser.add_argument("type", type=str, choices=["all", "apt", "rosdep"], help="The type of packages to install.") @@ -125,15 +126,17 @@ def install_rosdep_packages(paths: list[str], ros_distro: str = "humble"): run_and_print(["rosdep", "init"]) run_and_print(["rosdep", "update", f"--rosdistro={ros_distro}"]) # install rosdep packages - run_and_print([ - "rosdep", - "install", - "--from-paths", - f"{ws_path}/src", - "--ignore-src", - "-y", - f"--rosdistro={ros_distro}", - ]) + run_and_print( + [ + "rosdep", + "install", + "--from-paths", + f"{ws_path}/src", + "--ignore-src", + "-y", + f"--rosdistro={ros_distro}", + ] + ) else: print(f"[INFO] No rosdep packages specified for the extension at: {path}") else: diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py index ba57cc17ed3c..bbec83183584 100644 --- a/tools/run_all_tests.py +++ b/tools/run_all_tests.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -29,6 +29,7 @@ import time from datetime import datetime from pathlib import Path + from prettytable import PrettyTable # Local imports diff --git a/tools/run_train_envs.py b/tools/run_train_envs.py index 2f126bca6ec7..efc85c0265ba 100644 --- a/tools/run_train_envs.py +++ b/tools/run_train_envs.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tools/template/__init__.py b/tools/template/__init__.py index 2e924fbf1b13..460a30569089 100644 --- a/tools/template/__init__.py +++ b/tools/template/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tools/template/cli.py b/tools/template/cli.py index 013519f2a89e..d922025e070f 100644 --- a/tools/template/cli.py +++ b/tools/template/cli.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -73,7 +73,7 @@ def input_checkbox(self, message: str, choices: list[str], default: str | None = def transformer(result: list[str]) -> str: if "all" in result or "both" in result: token = "all" if "all" in result else "both" - return f'{token} ({", ".join(choices[:choices.index("---")])})' + return f"{token} ({', '.join(choices[: choices.index('---')])})" return ", ".join(result) return inquirer.checkbox( diff --git a/tools/template/common.py b/tools/template/common.py index 11d186991ab3..08d2732a1911 100644 --- a/tools/template/common.py +++ b/tools/template/common.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tools/template/generator.py b/tools/template/generator.py index 5055e56dcf3d..04f4bae6f63d 100644 --- a/tools/template/generator.py +++ b/tools/template/generator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -96,22 +96,22 @@ def _generate_task_per_workflow(task_dir: str, specification: dict) -> None: # workflow-specific content if task_spec["workflow"]["name"] == "direct": # - task/*env_cfg.py - template = jinja_env.get_template(f'tasks/direct_{task_spec["workflow"]["type"]}/env_cfg') + template = jinja_env.get_template(f"tasks/direct_{task_spec['workflow']['type']}/env_cfg") _write_file( - os.path.join(task_dir, f'{task_spec["filename"]}_env_cfg.py'), content=template.render(**specification) + os.path.join(task_dir, f"{task_spec['filename']}_env_cfg.py"), content=template.render(**specification) ) # - task/*env.py - template = jinja_env.get_template(f'tasks/direct_{task_spec["workflow"]["type"]}/env') - _write_file(os.path.join(task_dir, f'{task_spec["filename"]}_env.py'), content=template.render(**specification)) + template = jinja_env.get_template(f"tasks/direct_{task_spec['workflow']['type']}/env") + _write_file(os.path.join(task_dir, f"{task_spec['filename']}_env.py"), content=template.render(**specification)) elif task_spec["workflow"]["name"] == "manager-based": # - task/*env_cfg.py - template = jinja_env.get_template(f'tasks/manager-based_{task_spec["workflow"]["type"]}/env_cfg') + template = jinja_env.get_template(f"tasks/manager-based_{task_spec['workflow']['type']}/env_cfg") _write_file( - os.path.join(task_dir, f'{task_spec["filename"]}_env_cfg.py'), content=template.render(**specification) + os.path.join(task_dir, f"{task_spec['filename']}_env_cfg.py"), content=template.render(**specification) ) # - task/mdp folder shutil.copytree( - os.path.join(TEMPLATE_DIR, "tasks", f'manager-based_{task_spec["workflow"]["type"]}', "mdp"), + os.path.join(TEMPLATE_DIR, "tasks", f"manager-based_{task_spec['workflow']['type']}", "mdp"), os.path.join(task_dir, "mdp"), dirs_exist_ok=True, ) @@ -161,7 +161,7 @@ def _external(specification: dict) -> None: # repo files print(" |-- Copying repo files...") shutil.copyfile(os.path.join(ROOT_DIR, ".dockerignore"), os.path.join(project_dir, ".dockerignore")) - shutil.copyfile(os.path.join(ROOT_DIR, ".flake8"), os.path.join(project_dir, ".flake8")) + shutil.copyfile(os.path.join(ROOT_DIR, "pyproject.toml"), os.path.join(project_dir, "pyproject.toml")) shutil.copyfile(os.path.join(ROOT_DIR, ".gitattributes"), os.path.join(project_dir, ".gitattributes")) if os.path.exists(os.path.join(ROOT_DIR, ".gitignore")): shutil.copyfile(os.path.join(ROOT_DIR, ".gitignore"), os.path.join(project_dir, ".gitignore")) @@ -184,10 +184,12 @@ def _external(specification: dict) -> None: # replace placeholder in scripts for file in glob.glob(os.path.join(dir, rl_library["name"], "*.py")): _replace_in_file( - [( - "# PLACEHOLDER: Extension template (do not remove this comment)", - f"import {name}.tasks # noqa: F401", - )], + [ + ( + "# PLACEHOLDER: Extension template (do not remove this comment)", + f"import {name}.tasks # noqa: F401", + ) + ], src=file, ) # - other scripts @@ -198,10 +200,12 @@ def _external(specification: dict) -> None: ) for script in ["zero_agent.py", "random_agent.py"]: _replace_in_file( - [( - "# PLACEHOLDER: Extension template (do not remove this comment)", - f"import {name}.tasks # noqa: F401", - )], + [ + ( + "# PLACEHOLDER: Extension template (do not remove this comment)", + f"import {name}.tasks # noqa: F401", + ) + ], src=os.path.join(ROOT_DIR, "scripts", "environments", script), dst=os.path.join(dir, script), ) @@ -279,9 +283,9 @@ def get_algorithms_per_rl_library(single_agent: bool = True, multi_agent: bool = basename = os.path.basename(file).replace("_cfg", "") if basename.startswith(f"{rl_library}_"): algorithm = basename.replace(f"{rl_library}_", "").upper() - assert ( - algorithm in SINGLE_AGENT_ALGORITHMS or algorithm in MULTI_AGENT_ALGORITHMS - ), f"{algorithm} algorithm is not listed in the supported algorithms" + assert algorithm in SINGLE_AGENT_ALGORITHMS or algorithm in MULTI_AGENT_ALGORITHMS, ( + f"{algorithm} algorithm is not listed in the supported algorithms" + ) if single_agent and algorithm in SINGLE_AGENT_ALGORITHMS: data[rl_library].append(algorithm) if multi_agent and algorithm in MULTI_AGENT_ALGORITHMS: diff --git a/tools/template/templates/extension/config/extension.toml b/tools/template/templates/extension/config/extension.toml index 66230f334dd5..dbe4b064fbc4 100644 --- a/tools/template/templates/extension/config/extension.toml +++ b/tools/template/templates/extension/config/extension.toml @@ -25,7 +25,7 @@ keywords = ["extension", "template", "isaaclab"] [[python.module]] name = "{{ name }}" -[isaaclab_settings] +[isaac_lab_settings] # TODO: Uncomment and list any apt dependencies here. # If none, leave it commented out. # apt_deps = ["example_package"] diff --git a/tools/template/templates/extension/setup.py b/tools/template/templates/extension/setup.py index c4c68f4b056f..e991708e34d8 100644 --- a/tools/template/templates/extension/setup.py +++ b/tools/template/templates/extension/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -6,8 +6,8 @@ """Installation script for the '{{ name }}' python package.""" import os -import toml +import toml from setuptools import setup # Obtain the extension data from the extension.toml file diff --git a/tools/template/templates/extension/ui_extension_example.py b/tools/template/templates/extension/ui_extension_example.py index 6531acfb20ef..483f323954a7 100644 --- a/tools/template/templates/extension/ui_extension_example.py +++ b/tools/template/templates/extension/ui_extension_example.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tools/template/templates/external/.vscode/tools/settings.template.json b/tools/template/templates/external/.vscode/tools/settings.template.json index 5b97ac267e51..c1528d65dd73 100644 --- a/tools/template/templates/external/.vscode/tools/settings.template.json +++ b/tools/template/templates/external/.vscode/tools/settings.template.json @@ -55,15 +55,8 @@ ], // This enables python language server. Seems to work slightly better than jedi: "python.languageServer": "Pylance", - // We use "black" as a formatter: - "python.formatting.provider": "black", - "python.formatting.blackArgs": ["--line-length", "120"], - // Use flake8 for linting - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "python.linting.flake8Args": [ - "--max-line-length=120" - ], + // Use ruff as a formatter and linter + "ruff.configuration": "${workspaceFolder}/pyproject.toml", // Use docstring generator "autoDocstring.docstringFormat": "google", "autoDocstring.guessTypes": true, diff --git a/tools/template/templates/external/.vscode/tools/setup_vscode.py b/tools/template/templates/external/.vscode/tools/setup_vscode.py index 3a96361dffda..f8c61b76c5b7 100644 --- a/tools/template/templates/external/.vscode/tools/setup_vscode.py +++ b/tools/template/templates/external/.vscode/tools/setup_vscode.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tools/template/templates/external/docker/docker-compose.yaml b/tools/template/templates/external/docker/docker-compose.yaml index 7ad799c100d5..f1505b90d225 100644 --- a/tools/template/templates/external/docker/docker-compose.yaml +++ b/tools/template/templates/external/docker/docker-compose.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tools/template/templates/tasks/manager-based_single-agent/mdp/__init__.py b/tools/template/templates/tasks/manager-based_single-agent/mdp/__init__.py index 6b43c271164e..966d4a3f4b7c 100644 --- a/tools/template/templates/tasks/manager-based_single-agent/mdp/__init__.py +++ b/tools/template/templates/tasks/manager-based_single-agent/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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/tools/template/templates/tasks/manager-based_single-agent/mdp/rewards.py b/tools/template/templates/tasks/manager-based_single-agent/mdp/rewards.py index ceb3956996cf..5500089d7f94 100644 --- a/tools/template/templates/tasks/manager-based_single-agent/mdp/rewards.py +++ b/tools/template/templates/tasks/manager-based_single-agent/mdp/rewards.py @@ -1,13 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -import torch from typing import TYPE_CHECKING +import torch + from isaaclab.assets import Articulation from isaaclab.managers import SceneEntityCfg from isaaclab.utils.math import wrap_to_pi diff --git a/tools/test_settings.py b/tools/test_settings.py index 7ba3f6e3def7..3738eab3d2e0 100644 --- a/tools/test_settings.py +++ b/tools/test_settings.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# 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 @@ -34,6 +34,8 @@ "test_operational_space.py": 500, "test_non_headless_launch.py": 1000, # This test launches the app in non-headless mode and starts simulation "test_rl_games_wrapper.py": 500, + "test_skrl_wrapper.py": 500, + "test_rsl_rl_wrapper.py": 500, } """A dictionary of tests and their timeouts in seconds.