diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b10299b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# ignore .git related folders +.git/ +.github/ +.gitignore +# ignore docs +docs/ +# copy in licenses folder to the container +!docs/licenses/ +# ignore logs +**/logs/ +**/runs/ +**/output/* +**/outputs/* +**/videos/* +**/wandb/* +*.tmp +# ignore docker +docker/cluster/exports/ +docker/.container.cfg +# ignore recordings +recordings/ +# ignore __pycache__ +**/__pycache__/ +**/*.egg-info/ +# ignore local virtual environments +env_uwlab/ +# Note: _isaaclab/ is NOT ignored so it gets synced to cluster +# ignore isaac sim symlink +_isaac_sim? +# Docker history +docker/.uw-lab-docker-history +# datasets +/datasets/ diff --git a/.flake8 b/.flake8 index bf8ef70..9b4a023 100644 --- a/.flake8 +++ b/.flake8 @@ -5,14 +5,14 @@ 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 -# W605: Invalid escape sequence # 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. -ignore=E402,E501,W503,W605,E203,D401,R504,R505,SIM102,SIM117 +# 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/** diff --git a/.gitattributes b/.gitattributes index 827ccf1..e3c0ead 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,18 +1,14 @@ -# copied and modified from https://github.com/NVIDIA/warp/blob/main/.gitattributes -* text=auto -*.sh text eol=LF - -# copied from https://github.com/isaac-orbit/isaaclab_assets/blob/main/.gitattributes *.usd filter=lfs diff=lfs merge=lfs -text +*.usda filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text +*.hdr filter=lfs diff=lfs merge=lfs -text *.dae filter=lfs diff=lfs merge=lfs -text *.mtl filter=lfs diff=lfs merge=lfs -text *.obj filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text -*.png filter=lfs diff=lfs merge=lfs -text -*.jpg filter=lfs diff=lfs merge=lfs -text -*.psd filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text -*.usda filter=lfs diff=lfs merge=lfs -text -*.hdr filter=lfs diff=lfs merge=lfs -text *.pt filter=lfs diff=lfs merge=lfs -text *.jit filter=lfs diff=lfs merge=lfs -text +*.hdf5 filter=lfs diff=lfs merge=lfs -text + +*.bat text eol=crlf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..42c57ca --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Codeowners are designated by their GitHub username. They are +# the people who are responsible for reviewing and approving PRs +# that modify the files that match the pattern. +# +# Codeowners are not the same as contributors. They are not +# automatically added to the PR, but they will be requested to +# review the PR when it is created. +# +# As a general rule, the codeowners are the people who are +# most familiar with the code that the PR is modifying. If you +# are not sure who to add, ask in the issue or in the PR itself. +# +# The format of the file is as follows: +# + +# Core Framework +/source/uwlab @zoctipus + +# RL Environment +/source/uwlab_tasks @zoctipus + +# Assets +/source/uwlab_assets @zoctipus + +# RL +/source/uwlab_rl @zoctipus @patrickhaoy + +# Application +/source/uwlab_apps @zoctipus @patrickhaoy + +# Standalone Scripts +/scripts @zoctipus +/scripts_v2 @zoctipus + +# Github Actions +/.github/ @zoctipus + +# Visual Studio Code +/.vscode/ @zoctipus + +# Infrastructure (Docker, Docs, Tools) +/docker/ @zoctipus +/docs/ @zoctipus +/tools/ @zoctipus diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..54d6f21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,54 @@ +--- +name: Bug Report +about: Submit a bug report +title: "[Bug Report] Bug title" + +--- + +If you are submitting a bug report, please fill in the following details and use the tag [bug]. + +### Describe the bug + +A clear and concise description of what the bug is. + +### Steps to reproduce + +Please try to provide a minimal example to reproduce the bug. Error messages and stack traces are also helpful. + + + +### System Info + +Describe the characteristic of your environment: + + +- Commit: [e.g. 8f3b9ca] +- Isaac Sim Version: [e.g. 5.0, this can be obtained by `cat ${ISAACSIM_PATH}/VERSION`] +- OS: [e.g. Ubuntu 22.04] +- GPU: [e.g. RTX 5090] +- CUDA: [e.g. 12.8] +- GPU Driver: [e.g. 553.05, this can be seen by using `nvidia-smi` command.] + +### Additional context + +Add any other context about the problem here. + +### Checklist + +- [ ] I have checked that there is no similar issue in the repo (**required**) +- [ ] I have checked that the issue is not in running Isaac Sim itself and is related to the repo + +### Acceptance Criteria + +Add the criteria for which this task is considered **done**. If not known at issue creation time, you can add this once the issue is assigned. + +- [ ] Criteria 1 +- [ ] Criteria 2 diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 0000000..7437252 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,45 @@ +--- +name: Proposal +about: Propose changes that are not bug fixes +title: "[Proposal] Proposal title" +--- + + +### Proposal + +A clear and concise description of the proposal. In a few sentences, describe the feature and its core capabilities. + +### Motivation + +Please outline the motivation for the proposal. Summarize the core use cases and user problems and needs you are trying to solve. + +Is your feature request related to a problem? e.g.,"I'm always frustrated when [...]". + +If this is related to another GitHub issue, please link here too. + +### Alternatives + +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: + + +- UW Lab Version: [e.g. 2.3.0] +- 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. + +### Checklist + +- [ ] I have checked that there is no similar issue in the repo (**required**) + +### Acceptance Criteria + +Add the criteria for which this task is considered **done**. If not known at issue creation time, you can add this once the issue is assigned. + +- [ ] Criteria 1 +- [ ] Criteria 2 diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..d797a5c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,21 @@ +--- +name: Question +about: Ask a question +title: "[Question] Question title" +--- + +### Question + +Basic questions, related to robot learning, that are not bugs or feature requests will be closed without reply, because GitHub issues are not an appropriate venue for these. + +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://forums.developer.nvidia.com/c/omniverse/simulation/69). + +### Build Info + +Describe the versions that you are currently using: + + +- UW Lab Version: [e.g. 2.3.0] +- Isaac Sim Version: [e.g. 5.1, this can be obtained by `cat ${ISAACSIM_PATH}/VERSION`] diff --git a/.github/LICENSE_HEADER.md b/.github/LICENSE_HEADER.md new file mode 100644 index 0000000..981c64d --- /dev/null +++ b/.github/LICENSE_HEADER.md @@ -0,0 +1,4 @@ +Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +All Rights Reserved. + +SPDX-License-Identifier: BSD-3-Clause diff --git a/.github/LICENSE_HEADER.txt b/.github/LICENSE_HEADER.txt new file mode 100644 index 0000000..981c64d --- /dev/null +++ b/.github/LICENSE_HEADER.txt @@ -0,0 +1,4 @@ +Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +All Rights Reserved. + +SPDX-License-Identifier: BSD-3-Clause diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..28c31d1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,59 @@ +# Description + + + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. +List any dependencies that are required for this change. + +Fixes # (issue) + + + +## Type of change + + + +- Bug fix (non-breaking change which fixes an issue) +- New feature (non-breaking change which adds functionality) +- Breaking change (existing functionality will not work without user modification) +- Documentation update + +## Screenshots + +Please attach before and after screenshots of the change if applicable. + + + +## Checklist + +- [ ] I have read and understood the [contribution guidelines](https://uw-lab.github.io/UWLab/main/source/refs/contributing.html) +- [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./uwlab.sh --format` +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file +- [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there + + diff --git a/.github/actions/combine-results/action.yml b/.github/actions/combine-results/action.yml new file mode 100644 index 0000000..0145427 --- /dev/null +++ b/.github/actions/combine-results/action.yml @@ -0,0 +1,103 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: 'Combine XML Test Results' +description: 'Combines multiple XML test result files into a single file' + +inputs: + tests-dir: + description: 'Directory containing test result files' + default: 'tests' + required: false + output-file: + description: 'Output combined XML file path' + required: true + reports-dir: + description: 'Directory to store the combined results' + default: 'reports' + required: false + +runs: + using: composite + steps: + - name: Combine XML Test Results + shell: sh + run: | + # Function to combine multiple XML test results + combine_xml_results() { + local tests_dir="$1" + local output_file="$2" + local reports_dir="$3" + + echo "Combining test results from: $tests_dir" + echo "Output file: $output_file" + echo "Reports directory: $reports_dir" + + # Check if reports directory exists + if [ ! -d "$reports_dir" ]; then + echo "⚠️ Reports directory does not exist: $reports_dir" + mkdir -p "$reports_dir" + fi + + # Check if tests directory exists + if [ ! -d "$tests_dir" ]; then + echo "⚠️ Tests directory does not exist: $tests_dir" + echo "Creating fallback XML..." + echo 'Tests directory was not found' > "$output_file" + return + fi + + # Find all XML files in the tests directory + echo "Searching for XML files in: $tests_dir" + xml_files=$(find "$tests_dir" -name "*.xml" -type f 2>/dev/null | sort) + + if [ -z "$xml_files" ]; then + echo "⚠️ No XML files found in: $tests_dir" + echo "Creating fallback XML..." + echo 'No XML test result files were found' > "$output_file" + return + fi + + # Count XML files found + file_count=$(echo "$xml_files" | wc -l) + echo "βœ… Found $file_count XML file(s):" + echo "$xml_files" | while read -r file; do + echo " - $file ($(wc -c < "$file") bytes)" + done + + # Create combined XML + echo "πŸ”„ Combining $file_count XML files..." + echo '' > "$output_file" + echo '' >> "$output_file" + + # Process each XML file + combined_count=0 + echo "$xml_files" | while read -r file; do + if [ -f "$file" ]; then + echo " Processing: $file" + # Remove XML declaration and outer testsuites wrapper from each file + # Remove first line (XML declaration) and strip outer / tags + sed '1d; s/^//; s/<\/testsuites>$//' "$file" >> "$output_file" 2>/dev/null || { + echo " ⚠️ Warning: Could not process $file, skipping..." + } + combined_count=$((combined_count + 1)) + fi + done + + echo '' >> "$output_file" + echo "βœ… Successfully combined $combined_count files into: $output_file" + + # Verify output file was created + if [ -f "$output_file" ]; then + echo "βœ… Final output file created: $output_file" + echo "πŸ“Š Output file size: $(wc -c < "$output_file") bytes" + else + echo "❌ Failed to create output file: $output_file" + exit 1 + fi + } + + # Call the function with provided parameters + combine_xml_results "${{ inputs.tests-dir }}" "${{ inputs.output-file }}" "${{ inputs.reports-dir }}" diff --git a/.github/actions/docker-build/action.yml b/.github/actions/docker-build/action.yml new file mode 100644 index 0000000..fe0ce1e --- /dev/null +++ b/.github/actions/docker-build/action.yml @@ -0,0 +1,78 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: 'Build Docker Image' +description: 'Builds a Docker image with IsaacSim and UWLab dependencies' + +inputs: + image-tag: + description: 'Docker image tag to use' + required: true + isaacsim-base-image: + description: 'IsaacSim base image' + required: true + isaacsim-version: + description: 'IsaacSim version' + required: true + dockerfile-path: + description: 'Path to Dockerfile' + default: 'docker/Dockerfile.base' + required: false + context-path: + description: 'Build context path' + default: '.' + required: false + +runs: + using: composite + steps: + - name: NGC Login + shell: sh + run: | + # Only attempt NGC login if API key is available + if [ -n "${{ env.NGC_API_KEY }}" ]; then + echo "Logging into NGC registry..." + docker login -u \$oauthtoken -p ${{ env.NGC_API_KEY }} nvcr.io + echo "βœ… Successfully logged into NGC registry" + else + echo "⚠️ NGC_API_KEY not available - skipping NGC login" + echo "This is normal for PRs from forks or when secrets are not configured" + fi + + - name: Build Docker Image + shell: sh + run: | + # Function to build Docker image + build_docker_image() { + local image_tag="$1" + local isaacsim_base_image="$2" + local isaacsim_version="$3" + local dockerfile_path="$4" + local context_path="$5" + + echo "Building Docker image: $image_tag" + echo "Using Dockerfile: $dockerfile_path" + echo "Build context: $context_path" + + # Build Docker image + docker buildx build --progress=plain --platform linux/amd64 \ + -t uw-lab-dev \ + -t $image_tag \ + --build-arg ISAACSIM_BASE_IMAGE_ARG="$isaacsim_base_image" \ + --build-arg ISAACSIM_VERSION_ARG="$isaacsim_version" \ + --build-arg ISAACSIM_ROOT_PATH_ARG=/isaac-sim \ + --build-arg UWLAB_PATH_ARG=/workspace/uwlab \ + --build-arg DOCKER_USER_HOME_ARG=/root \ + --cache-from type=gha \ + --cache-to type=gha,mode=max \ + -f $dockerfile_path \ + --load $context_path + + echo "βœ… Docker image built successfully: $image_tag" + docker images | grep uw-lab-dev + } + + # Call the function with provided parameters + build_docker_image "${{ inputs.image-tag }}" "${{ inputs.isaacsim-base-image }}" "${{ inputs.isaacsim-version }}" "${{ inputs.dockerfile-path }}" "${{ inputs.context-path }}" diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 0000000..efe0ed9 --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,157 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: 'Run Tests in Docker Container' +description: 'Runs pytest tests in a Docker container with GPU support and result collection' + +inputs: + test-path: + description: 'Path to test directory or pytest arguments' + required: true + result-file: + description: 'Name of the result XML file' + required: true + container-name: + description: 'Name for the Docker container' + required: true + image-tag: + description: 'Docker image tag to use' + required: true + reports-dir: + description: 'Directory to store test results' + default: 'reports' + required: false + pytest-options: + description: 'Additional pytest options (e.g., -k filter)' + default: '' + required: false + filter-pattern: + description: 'Pattern to filter test files (e.g., uwlab_tasks)' + default: '' + required: false + +runs: + using: composite + steps: + - name: Run Tests in Docker Container + shell: bash + run: | + # Function to run tests in Docker container + run_tests() { + local test_path="$1" + local result_file="$2" + local container_name="$3" + local image_tag="$4" + local reports_dir="$5" + local pytest_options="$6" + local filter_pattern="$7" + + echo "Running tests in: $test_path" + if [ -n "$pytest_options" ]; then + echo "With pytest options: $pytest_options" + fi + if [ -n "$filter_pattern" ]; then + echo "With filter pattern: $filter_pattern" + fi + + # Create reports directory + mkdir -p "$reports_dir" + + # Clean up any existing container + docker rm -f $container_name 2>/dev/null || true + + # Build Docker environment variables + docker_env_vars="\ + -e OMNI_KIT_ACCEPT_EULA=yes \ + -e ACCEPT_EULA=Y \ + -e OMNI_KIT_DISABLE_CUP=1 \ + -e ISAAC_SIM_HEADLESS=1 \ + -e ISAAC_SIM_LOW_MEMORY=1 \ + -e PYTHONUNBUFFERED=1 \ + -e PYTHONIOENCODING=utf-8 \ + -e TEST_RESULT_FILE=$result_file" + + if [ -n "$filter_pattern" ]; then + if [[ "$filter_pattern" == not* ]]; then + # Handle "not pattern" case + exclude_pattern="${filter_pattern#not }" + docker_env_vars="$docker_env_vars -e TEST_EXCLUDE_PATTERN=$exclude_pattern" + echo "Setting exclude pattern: $exclude_pattern" + else + # Handle positive pattern case + docker_env_vars="$docker_env_vars -e TEST_FILTER_PATTERN=$filter_pattern" + echo "Setting include pattern: $filter_pattern" + fi + else + echo "No filter pattern provided" + fi + + echo "Docker environment variables: '$docker_env_vars'" + + # Run tests in container with error handling + echo "πŸš€ Starting Docker container for tests..." + if docker run --name $container_name \ + --entrypoint bash --gpus all --network=host \ + --security-opt=no-new-privileges:true \ + --memory=$(echo "$(free -m | awk '/^Mem:/{print $2}') * 0.9 / 1" | bc)m \ + --cpus=$(echo "$(nproc) * 0.9" | bc) \ + --oom-kill-disable=false \ + --ulimit nofile=65536:65536 \ + --ulimit nproc=4096:4096 \ + $docker_env_vars \ + $image_tag \ + -c " + set -e + cd /workspace/uwlab + mkdir -p tests + echo 'Starting pytest with path: $test_path' + /isaac-sim/python.sh -m pytest --ignore=tools/conftest.py $test_path $pytest_options -v --junitxml=tests/$result_file || echo 'Pytest completed with exit code: $?' + "; then + echo "βœ… Docker container completed successfully" + else + echo "⚠️ Docker container failed, but continuing to copy results..." + fi + + # Copy test results with error handling + echo "πŸ“‹ Attempting to copy test results..." + if docker cp $container_name:/workspace/uwlab/tests/$result_file "$reports_dir/$result_file" 2>/dev/null; then + echo "βœ… Test results copied successfully" + else + echo "❌ Failed to copy specific result file, trying to copy all test results..." + if docker cp $container_name:/workspace/uwlab/tests/ "$reports_dir/" 2>/dev/null; then + echo "βœ… All test results copied successfully" + # Look for any XML files and use the first one found + if [ -f "$reports_dir/full_report.xml" ]; then + mv "$reports_dir/full_report.xml" "$reports_dir/$result_file" + echo "βœ… Found and renamed full_report.xml to $result_file" + elif [ -f "$reports_dir/test-reports-"*".xml" ]; then + # Combine individual test reports if no full report exists + echo "πŸ“Š Combining individual test reports..." + echo '' > "$reports_dir/$result_file" + for xml_file in "$reports_dir"/test-reports-*.xml; do + if [ -f "$xml_file" ]; then + echo " Processing: $xml_file" + sed '1d; /^> "$reports_dir/$result_file" 2>/dev/null || true + fi + done + echo '' >> "$reports_dir/$result_file" + echo "βœ… Combined individual test reports into $result_file" + else + echo "❌ No test result files found, creating fallback" + echo "Container may have failed to generate any results" > "$reports_dir/$result_file" + fi + else + echo "❌ Failed to copy any test results, creating fallback" + echo "Container may have failed to generate results" > "$reports_dir/$result_file" + fi + fi + + # Clean up container + echo "🧹 Cleaning up Docker container..." + docker rm $container_name 2>/dev/null || echo "⚠️ Container cleanup failed, but continuing..." + } + + # Call the function with provided parameters + run_tests "${{ inputs.test-path }}" "${{ inputs.result-file }}" "${{ inputs.container-name }}" "${{ inputs.image-tag }}" "${{ inputs.reports-dir }}" "${{ inputs.pytest-options }}" "${{ inputs.filter-pattern }}" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..235dd3c --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,59 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Documentation-related changes +documentation: + - all: + - changed-files: + - any-glob-to-any-file: + - 'docs/**' + - '**/README.md' + - all-globs-to-all-files: + - '!docs/licenses/**' + +# Infrastructure changes +infrastructure: + - changed-files: + - any-glob-to-any-file: + - .github/** + - docker/** + - .dockerignore + - tools/** + - .vscode/** + - environment.yml + - setup.py + - pyproject.toml + - .pre-commit-config.yaml + - .flake8 + - uwlab.sh + - uwlab.bat + - docs/licenses/** + +# Assets (USD, glTF, etc.) related changes. +asset: + - changed-files: + - any-glob-to-any-file: + - source/uwlab_assets/** + +# Core related changes. +core: + - all: + - changed-files: + - any-glob-to-any-file: + - source/** + - scripts/** + - scripts_v2/** + - all-globs-to-all-files: + - '!source/uwlab_assets/**' + +# Add 'enhancement' label to any PR where the head branch name +# starts with `feature` or has a `feature` section in the name +enhancement: + - head-branch: ['^feature', 'feature'] + +# Add 'bug' label to any PR where the head branch name +# starts with `fix`/`bug` or has a `fix`/`bug` section in the name +bug: + - head-branch: ['^fix', 'fix', '^bug', 'bug'] diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..d0b0920 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,67 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 14 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: + - more-information-needed + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - pinned + - security + - "[Status] Maybe Later" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: true + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: true + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +# closeComment: > +# Your comment here. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 + +# Limit to only `issues` or `pulls` +only: issues + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..221698e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,242 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: Build and Test + +on: + pull_request: + branches: + - devel + - main + - 'release/**' + +# Concurrency control to prevent parallel runs on the same PR +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + checks: write + issues: read + +env: + NGC_API_KEY: ${{ secrets.NGC_API_KEY }} + ISAACSIM_BASE_IMAGE: ${{ vars.ISAACSIM_BASE_IMAGE || 'nvcr.io/nvidia/isaac-sim' }} + ISAACSIM_BASE_VERSION: ${{ vars.ISAACSIM_BASE_VERSION || '5.1.0' }} + DOCKER_IMAGE_TAG: uw-lab-dev:${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}-${{ github.sha }} + +jobs: + test-uwlab-tasks: + runs-on: [self-hosted, gpu, 24g] + timeout-minutes: 180 + continue-on-error: true + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Remove previous uw-lab-dev images + run: | + IDS=$(docker image ls -q uw-lab-dev 2>/dev/null || true) + if [ -n "$IDS" ]; then + echo "Removing existing uw-lab-dev images: $IDS" + docker image rm -f $IDS || true + else + echo "No existing uw-lab-dev images to remove." + fi + + - name: Build Docker Image + uses: ./.github/actions/docker-build + with: + image-tag: ${{ env.DOCKER_IMAGE_TAG }} + isaacsim-base-image: ${{ env.ISAACSIM_BASE_IMAGE }} + isaacsim-version: ${{ env.ISAACSIM_BASE_VERSION }} + + - name: Run UWLab Tasks Tests + uses: ./.github/actions/run-tests + with: + test-path: "tools" + result-file: "uwlab-tasks-report.xml" + container-name: "uw-lab-tasks-test-$$" + image-tag: ${{ env.DOCKER_IMAGE_TAG }} + pytest-options: "" + filter-pattern: "uwlab_tasks" + + - name: Copy Test Results from UWLab Tasks Container + run: | + CONTAINER_NAME="uw-lab-tasks-test-$$" + if docker ps -a | grep -q $CONTAINER_NAME; then + echo "Copying test results from UWLab Tasks container..." + docker cp $CONTAINER_NAME:/workspace/uwlab/tests/uwlab-tasks-report.xml reports/ 2>/dev/null || echo "No test results to copy from UWLab Tasks container" + fi + + - name: Upload UWLab Tasks Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: uwlab-tasks-test-results + path: reports/uwlab-tasks-report.xml + retention-days: 1 + compression-level: 9 + + - name: Check Test Results for Fork PRs + if: github.event.pull_request.head.repo.full_name != github.repository + run: | + if [ -f "reports/uwlab-tasks-report.xml" ]; then + # Check if the test results contain any failures + if grep -q 'failures="[1-9]' reports/uwlab-tasks-report.xml || grep -q 'errors="[1-9]' reports/uwlab-tasks-report.xml; then + echo "Tests failed for PR from fork. The test report is in the logs. Failing the job." + exit 1 + fi + else + echo "No test results file found. This might indicate test execution failed." + exit 1 + fi + + test-general: + runs-on: [self-hosted, gpu] + timeout-minutes: 180 + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Remove previous uw-lab-dev images + run: | + IDS=$(docker image ls -q uw-lab-dev 2>/dev/null || true) + if [ -n "$IDS" ]; then + echo "Removing existing uw-lab-dev images: $IDS" + docker image rm -f $IDS || true + else + echo "No existing uw-lab-dev images to remove." + fi + + - name: Build Docker Image + uses: ./.github/actions/docker-build + with: + image-tag: ${{ env.DOCKER_IMAGE_TAG }} + isaacsim-base-image: ${{ env.ISAACSIM_BASE_IMAGE }} + isaacsim-version: ${{ env.ISAACSIM_BASE_VERSION }} + + - name: Run General Tests + id: run-general-tests + uses: ./.github/actions/run-tests + with: + test-path: "tools" + result-file: "general-tests-report.xml" + container-name: "uw-lab-general-test-$$" + image-tag: ${{ env.DOCKER_IMAGE_TAG }} + pytest-options: "" + filter-pattern: "not uwlab_tasks" + + - name: Copy Test Results from General Tests Container + run: | + CONTAINER_NAME="uw-lab-general-test-$$" + if docker ps -a | grep -q $CONTAINER_NAME; then + echo "Copying test results from General Tests container..." + docker cp $CONTAINER_NAME:/workspace/uwlab/tests/general-tests-report.xml reports/ 2>/dev/null || echo "No test results to copy from General Tests container" + fi + + - name: Upload General Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: general-test-results + path: reports/general-tests-report.xml + retention-days: 1 + compression-level: 9 + + - name: Check Test Results for Fork PRs + if: github.event.pull_request.head.repo.full_name != github.repository + run: | + if [ -f "reports/general-tests-report.xml" ]; then + # Check if the test results contain any failures + if grep -q 'failures="[1-9]' reports/general-tests-report.xml || grep -q 'errors="[1-9]' reports/general-tests-report.xml; then + echo "Tests failed for PR from fork. The test report is in the logs. Failing the job." + exit 1 + fi + else + echo "No test results file found. This might indicate test execution failed." + exit 1 + fi + + combine-results: + needs: [test-uwlab-tasks, test-general] + runs-on: [self-hosted, gpu] + if: always() + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: false + + - name: Create Reports Directory + run: | + mkdir -p reports + + - name: Download Test Results + uses: actions/download-artifact@v4 + with: + name: uwlab-tasks-test-results + path: reports/ + continue-on-error: true + + - name: Download General Test Results + uses: actions/download-artifact@v4 + with: + name: general-test-results + path: reports/ + + - name: Combine All Test Results + uses: ./.github/actions/combine-results + with: + tests-dir: "reports" + output-file: "reports/combined-results.xml" + + - name: Upload Combined Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: pr-${{ github.event.pull_request.number }}-combined-test-results + path: reports/combined-results.xml + retention-days: 7 + compression-level: 9 + + - name: Comment on Test Results + id: test-reporter + if: github.event.pull_request.head.repo.full_name == github.repository + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: "reports/combined-results.xml" + check_name: "Tests Summary" + comment_mode: changes + comment_title: "Test Results Summary" + report_individual_runs: false + deduplicate_classes_by_file_name: true + compare_to_earlier_commit: true + fail_on: errors + action_fail_on_inconclusive: true + + - name: Report Test Results + if: github.event.pull_request.head.repo.full_name == github.repository + uses: dorny/test-reporter@v1 + with: + name: UWLab Build and Test Results + path: reports/combined-results.xml + reporter: java-junit + fail-on-error: true + only-summary: false + max-annotations: '50' + report-title: "UWLab Test Results - ${{ github.workflow }}" diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 0000000..c4ffc43 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,120 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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/docs.yaml b/.github/workflows/docs.yaml index 881d2a6..6be3b02 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,9 +1,16 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + name: Build & deploy docs on: push: branches: - main + - devel + - 'release/**' pull_request: types: [opened, synchronize, reopened] @@ -20,9 +27,8 @@ jobs: steps: - id: trigger-deploy env: - REPO_NAME: "UW-Lab/UWLab" - BRANCH_REF: "refs/heads/main" - if: "${{ github.repository == env.REPO_NAME && github.ref == env.BRANCH_REF }}" + REPO_NAME: ${{ secrets.REPO_NAME }} + if: "${{ github.repository == env.REPO_NAME && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/devel' || startsWith(github.ref, 'refs/heads/release/')) }}" run: echo "defined=true" >> "$GITHUB_OUTPUT" build-docs: @@ -33,11 +39,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + lfs: true - name: Setup python uses: actions/setup-python@v2 with: - python-version: "3.10" + python-version: "3.11" architecture: x64 - name: Install dev requirements diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..aeb2be2 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,17 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 diff --git a/.github/workflows/license-check.yaml b/.github/workflows/license-check.yaml new file mode 100644 index 0000000..0a9d9ac --- /dev/null +++ b/.github/workflows/license-check.yaml @@ -0,0 +1,127 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: Check Python Dependency Licenses + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + license-check: + runs-on: [self-hosted] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + # - name: Install jq + # run: sudo apt-get update && sudo apt-get install -y jq + + - name: Clean up disk space + run: | + rm -rf /opt/hostedtoolcache + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' # Adjust as needed + + - name: Install dependencies using ./uwlab.sh -i + run: | + # first install isaac sim + pip install --upgrade pip + pip install 'isaacsim[all,extscache]==${{ vars.ISAACSIM_BASE_VERSION || '5.0.0' }}' --extra-index-url https://pypi.nvidia.com + chmod +x ./uwlab.sh # Make sure the script is executable + # install all lab dependencies + ./uwlab.sh -i + + - name: Install pip-licenses + run: | + pip install pip-licenses + pip install -r tools/template/requirements.txt + pip install -r docs/requirements.txt + + # Optional: Print the license report for visibility + - name: Print License Report + run: pip-licenses --from=mixed --format=markdown + + # Print pipdeptree + - name: Print pipdeptree + run: | + pip install pipdeptree + pipdeptree + + - name: Check licenses against whitelist and exceptions + run: | + # Generate licenses report and validate using concise Python + pip-licenses --from=mixed --format=json > licenses.json + python - <<'PY' + import json, sys + + # Substring-based allowlist (case-insensitive) + ALLOWED = ("mit", "apache", "bsd", "isc", "zlib", "psfl", "bsl", "boost", "mpl") + + def _norm(s: str) -> str: + return (s or "").strip().lower() + + def _is_allowed(license_str: str) -> bool: + ln = _norm(license_str) + return any(tok in ln for tok in ALLOWED) + + EXCEPTIONS_FILE = ".github/workflows/license-exceptions.json" + + # Load report (defensive) + try: + with open("licenses.json", encoding="utf-8") as f: + packages = json.load(f) + except Exception as e: + print(f"ERROR: Failed to parse licenses.json: {e}") + sys.exit(1) + + # Load exceptions (optional) + try: + with open(EXCEPTIONS_FILE, encoding="utf-8") as f: + exc_entries = json.load(f) + except FileNotFoundError: + exc_entries = [] + exc_map = { (e.get("package") or ""): e.get("license") for e in exc_entries if e.get("package") } + + failed = 0 + for rec in packages: + name = (rec.get("Name") or "").strip() + lic = (rec.get("License") or "").strip() + if not name or name.startswith("uwlab"): + continue + if _is_allowed(lic): + continue + if name in exc_map: + exc_lic = exc_map[name] + exc_norm = _norm(exc_lic) + lic_norm = _norm(lic) + # NEW: if detected license is unknown/blank, accept when an exception exists + if lic_norm in ("", "unknown"): + continue + # Treat blank/unknown exception license as wildcard (accept any) + if exc_norm in ("", "null", "unknown", "unspecified", "n/a", "na", "none"): + continue + # Flexible match to avoid churn on formatting differences + if exc_norm in lic_norm or lic_norm in exc_norm: + continue + print(f"ERROR: {name} has license: {lic}") + failed += 1 + continue + print(f"ERROR: {name} has license: {lic}") + failed += 1 + + if failed: + print(f"ERROR: {failed} packages were flagged.") + sys.exit(1) + print("All packages were checked.") + PY diff --git a/.github/workflows/license-exceptions.json b/.github/workflows/license-exceptions.json new file mode 100644 index 0000000..5e8e487 --- /dev/null +++ b/.github/workflows/license-exceptions.json @@ -0,0 +1,479 @@ +[ + { + "package": "uwlab", + "license": null, + "comment": "This Repo" + }, + { + "package": "uwlab_assets", + "license": null, + "comment": "This Repo" + }, + { + "package": "uwlab_apps", + "license": null, + "comment": "This Repo" + }, + { + "package": "uwlab_rl", + "license": null, + "comment": "This Repo" + }, + { + "package": "uwlab_tasks", + "license": null, + "comment": "This Repo" + }, + { + "package": "pytorch3d", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "aiobotocore", + "license": "UNKNOWN", + "comment": "Apache-2.0" + }, + { + "package": "isaaclab", + "license": null + }, + { + "package": "isaaclab_assets", + "license": null + }, + { + "package": "isaaclab_mimic", + "license": null + }, + { + "package": "isaaclab_rl", + "license": null + }, + { + "package": "isaaclab_tasks", + "license": null + }, + { + "package": "isaacsim", + "license": null + }, + { + "package": "isaacsim-app", + "license": null + }, + { + "package": "isaacsim-asset", + "license": null + }, + { + "package": "isaacsim-benchmark", + "license": null + }, + { + "package": "isaacsim-code-editor", + "license": null + }, + { + "package": "isaacsim-core", + "license": null + }, + { + "package": "isaacsim-cortex", + "license": null + }, + { + "package": "isaacsim-example", + "license": null + }, + { + "package": "isaacsim-extscache-kit", + "license": null + }, + { + "package": "isaacsim-extscache-kit-sdk", + "license": null + }, + { + "package": "isaacsim-extscache-physics", + "license": null + }, + { + "package": "isaacsim-gui", + "license": null + }, + { + "package": "isaacsim-kernel", + "license": null + }, + { + "package": "isaacsim-replicator", + "license": null + }, + { + "package": "isaacsim-rl", + "license": null + }, + { + "package": "isaacsim-robot", + "license": null + }, + { + "package": "isaacsim-robot-motion", + "license": null + }, + { + "package": "isaacsim-robot-setup", + "license": null + }, + { + "package": "isaacsim-ros1", + "license": null + }, + { + "package": "isaacsim-ros2", + "license": null + }, + { + "package": "isaacsim-sensor", + "license": null + }, + { + "package": "isaacsim-storage", + "license": null + }, + { + "package": "isaacsim-template", + "license": null + }, + { + "package": "isaacsim-test", + "license": null + }, + { + "package": "isaacsim-utils", + "license": null + }, + { + "package": "nvidia-cublas-cu12", + "license": null + }, + { + "package": "nvidia-cuda-cupti-cu12", + "license": null + }, + { + "package": "nvidia-cuda-nvrtc-cu12", + "license": null + }, + { + "package": "nvidia-cuda-runtime-cu12", + "license": null + }, + { + "package": "nvidia-cudnn-cu12", + "license": null + }, + { + "package": "nvidia-cufft-cu12", + "license": null + }, + { + "package": "nvidia-cufile-cu12", + "license": null + }, + { + "package": "nvidia-curand-cu12", + "license": null + }, + { + "package": "nvidia-cusolver-cu12", + "license": null + }, + { + "package": "nvidia-cusparse-cu12", + "license": null + }, + { + "package": "nvidia-cusparselt-cu12", + "license": null + }, + { + "package": "nvidia-nccl-cu12", + "license": null + }, + { + "package": "nvidia-nvjitlink-cu12", + "license": null + }, + { + "package": "nvidia-nvtx-cu12", + "license": null + }, + { + "package": "omniverse-kit", + "license": null + }, + { + "package": "warp-lang", + "license": null + }, + { + "package": "cmeel", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "cmeel-assimp", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "cmeel-boost", + "license": "BSL-1.0", + "comment": "BSL" + }, + { + "package": "cmeel-console-bridge", + "license": "Zlib", + "comment": "ZLIBL" + }, + { + "package": "cmeel-octomap", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "cmeel-qhull", + "license": "UNKNOWN", + "comment": "custom / OSRB" + }, + { + "package": "cmeel-tinyxml", + "license": "Zlib", + "comment": "ZLIBL" + }, + { + "package": "cmeel-urdfdom", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "cmeel-zlib", + "license": "Zlib", + "comment": "ZLIBL" + }, + { + "package": "matplotlib", + "license": "Python Software Foundation License" + }, + { + "package": "certifi", + "license": "Mozilla Public License 2.0 (MPL 2.0)" + }, + { + "package": "rl_games", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "robomimic", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "hpp-fcl", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "pin", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "eigenpy", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "qpsolvers", + "license": "GNU Lesser General Public License v3 (LGPLv3)", + "comment": "OSRB" + }, + { + "package": "quadprog", + "license": "GNU General Public License v2 or later (GPLv2+)", + "comment": "OSRB" + }, + { + "package": "Markdown", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "anytree", + "license": "UNKNOWN", + "comment": "Apache" + }, + { + "package": "click", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "egl_probe", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "filelock", + "license": "Unlicense", + "comment": "no condition" + }, + { + "package": "proglog", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "termcolor", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "typing_extensions", + "license": "Python Software Foundation License", + "comment": "PSFL / OSRB" + }, + { + "package": "urllib3", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "h5py", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "pillow", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "pygame", + "license": "GNU Library or Lesser General Public License (LGPL)", + "comment": "OSRB" + }, + { + "package": "scikit-learn", + "license": "UNKNOWN", + "comment": "BSD" + }, + { + "package": "tensorboardX", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "attrs", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "jsonschema", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "jsonschema-specifications", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "referencing", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "regex", + "license": "UNKNOWN", + "comment": "Apache 2.0" + }, + { + "package": "anyio", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package" : "hf-xet", + "license" : "UNKNOWN", + "comment": "Apache 2.0" + }, + { + "package": "rpds-py", + "license" : "UNKNOWN", + "comment": "MIT" + }, + { + "package": "typing-inspection", + "license" : "UNKNOWN", + "comment": "MIT" + }, + { + "package": "ml_dtypes", + "license" : "UNKNOWN", + "comment": "Apache 2.0" + }, + { + "package": "zipp", + "license" : "UNKNOWN", + "comment": "MIT" + }, + { + "package": "fsspec", + "license" : "UNKNOWN", + "comment": "BSD" + }, + { + "package": "numpy-quaternion", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "aiohappyeyeballs", + "license": "Other/Proprietary License; Python Software Foundation License", + "comment": "PSFL / OSRB" + }, + { + "package": "cffi", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "trio", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "pipdeptree", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "msgpack", + "license": "UNKNOWN", + "comment": "Apache 2.0" + }, + { + "package": "onnx-ir", + "license": "UNKNOWN", + "comment": "Apache 2.0" + }, + { + "package": "matplotlib-inline", + "license": "UNKNOWN", + "comment": "BSD-3" + } +] diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index d34fcde..964f783 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -1,9 +1,13 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + name: Run linters using pre-commit on: pull_request: - push: - branches: [main] + types: [opened, synchronize, reopened] jobs: pre-commit: @@ -11,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 e0b1848..0889c69 100644 --- a/.gitignore +++ b/.gitignore @@ -24,16 +24,15 @@ # Docker/Singularity **/*.sif -docker/exports/ -docker/.container.yaml +docker/cluster/exports/ +docker/.container.cfg # IDE **/.idea/ **/.vscode/ # Don't ignore the top-level .vscode directory as it is # used to configure VS Code settings - -code_log.md +!.vscode # Outputs **/output/* @@ -49,6 +48,8 @@ docker/artifacts/ **/generated/* # Isaac-Sim packman +/_isaaclab/ +/_uwlab/ _isaac_sim* _repo _build @@ -58,11 +59,15 @@ _build **/runs/* **/logs/* **/recordings/* -**/datasets/ -# node generated files -**/node_modules/* -**/dist/* +# Pre-Trained Checkpoints +/.pretrained_checkpoints/ + +# Teleop Recorded Dataset +/datasets/ + +# Tests +tests/ -# credentials -**/credentials/* +# Docker history +.uw-lab-docker-history diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d355d0..25a243e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,28 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +exclude: ^scripts/ repos: - repo: https://github.com/python/black - rev: 23.10.1 + rev: 24.3.0 hooks: - id: black - args: ["--line-length", "120", "--preview"] + args: ["--line-length", "120", "--unstable"] - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 additional_dependencies: [flake8-simplify, flake8-return] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: check-symlinks - id: destroyed-symlinks + - id: check-added-large-files + args: ["--maxkb=2000"] # restrict files more than 2 MB. Should use git-lfs instead. - id: check-yaml - id: check-merge-conflict - id: check-case-conflict @@ -25,22 +33,52 @@ repos: - id: detect-private-key - id: debug-statements - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort name: isort (python) args: ["--profile", "black", "--filter-files"] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade - args: ["--py37-plus"] + args: ["--py310-plus"] + # FIXME: This is a hack because Pytorch does not like: torch.Tensor | dict aliasing + exclude: "source/uwlab/uwlab/envs/common.py|source/uwlab/uwlab/ui/widgets/image_plot.py|source/uwlab_tasks/uwlab_tasks/direct/humanoid_amp/motions/motion_loader.py" - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell additional_dependencies: - tomli + exclude: "CONTRIBUTORS.md|docs/source/setup/walkthrough/concepts_env_design.rst" + # FIXME: Figure out why this is getting stuck under VPN. + # - repo: https://github.com/RobertCraigie/pyright-python + # rev: v1.1.315 + # hooks: + # - id: pyright + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.1 + hooks: + - id: insert-license + files: \.(py|ya?ml)$ + args: + # - --remove-header # Remove existing license headers. Useful when updating license. + - --license-filepath + - .github/LICENSE_HEADER.txt + - --use-current-year + exclude: "source/uwlab_mimic/|scripts/imitation_learning/uwlab_mimic/" + # Apache 2.0 license for mimic files + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.1 + hooks: + - id: insert-license + files: ^(source/uwlab_mimic|scripts/imitation_learning/uwlab_mimic)/.*\.py$ + args: + # - --remove-header # Remove existing license headers. Useful when updating license. + - --license-filepath + - .github/LICENSE_HEADER_MIMIC.txt + - --use-current-year - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/.vscode/.gitignore b/.vscode/.gitignore new file mode 100644 index 0000000..290b1bd --- /dev/null +++ b/.vscode/.gitignore @@ -0,0 +1,10 @@ +# Note: These files are kept for development purposes only. +!tools/settings.template.json +!tools/setup_vscode.py +!extensions.json +!launch.json +!tasks.json + +# Ignore all other files +.python.env +*.json diff --git a/.vscode/tools/setup_vscode.py b/.vscode/tools/setup_vscode.py new file mode 100644 index 0000000..cd3a690 --- /dev/null +++ b/.vscode/tools/setup_vscode.py @@ -0,0 +1,234 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This script sets up the vs-code settings for the Isaac Lab project. + +This script merges the python.analysis.extraPaths from the "{ISAACSIM_DIR}/.vscode/settings.json" file into +the ".vscode/settings.json" file. + +This is necessary because Isaac Sim 2022.2.1 onwards does not add the necessary python packages to the python path +when the "setup_python_env.sh" is run as part of the vs-code launch configuration. +""" + +import re +import sys +import os +import pathlib + + +UWLAB_DIR = pathlib.Path(__file__).parents[2] +"""Path to the Isaac Lab directory.""" + +try: + import isaacsim # noqa: F401 + + isaacsim_dir = os.environ.get("ISAAC_PATH", "") +except ModuleNotFoundError or ImportError: + isaacsim_dir = os.path.join(UWLAB_DIR, "_isaac_sim") +except EOFError: + print("Unable to trigger EULA acceptance. This is likely due to the script being run in a non-interactive shell.") + print("Please run the script in an interactive shell to accept the EULA.") + print("Skipping the setup of the VSCode settings...") + sys.exit(0) + +# check if the isaac-sim directory exists +if not os.path.exists(isaacsim_dir): + raise FileNotFoundError( + f"Could not find the isaac-sim directory: {isaacsim_dir}. There are two possible reasons for this:" + f"\n\t1. The Isaac Sim directory does not exist as a symlink at: {os.path.join(UWLAB_DIR, '_isaac_sim')}" + "\n\t2. The script could not import the 'isaacsim' package. This could be due to the 'isaacsim' package not " + "being installed in the Python environment.\n" + "\nPlease make sure that the Isaac Sim directory exists or that the 'isaacsim' package is installed." + ) + +ISAACSIM_DIR = isaacsim_dir +"""Path to the isaac-sim directory.""" + + +def overwrite_python_analysis_extra_paths(uwlab_settings: str) -> str: + """Overwrite the python.analysis.extraPaths in the Isaac Lab settings file. + + The extraPaths are replaced with the path names from the isaac-sim settings file that exists in the + "{ISAACSIM_DIR}/.vscode/settings.json" file. + + If the isaac-sim settings file does not exist, the extraPaths are not overwritten. + + Args: + uwlab_settings: The settings string to use as template. + + Returns: + The settings string with overwritten python analysis extra paths. + """ + # isaac-sim settings + isaacsim_vscode_filename = os.path.join(ISAACSIM_DIR, ".vscode", "settings.json") + + # we use the isaac-sim settings file to get the python.analysis.extraPaths for kit extensions + # if this file does not exist, we will not add any extra paths + if os.path.exists(isaacsim_vscode_filename): + # read the path names from the isaac-sim settings file + with open(isaacsim_vscode_filename) as f: + vscode_settings = f.read() + # extract the path names + # search for the python.analysis.extraPaths section and extract the contents + settings = re.search( + r"\"python.analysis.extraPaths\": \[.*?\]", vscode_settings, flags=re.MULTILINE | re.DOTALL + ) + settings = settings.group(0) + settings = settings.split('"python.analysis.extraPaths": [')[-1] + settings = settings.split("]")[0] + + # read the path names from the isaac-sim settings file + path_names = settings.split(",") + path_names = [path_name.strip().strip('"') for path_name in path_names] + path_names = [path_name for path_name in path_names if len(path_name) > 0] + + # change the path names to be relative to the Isaac Lab directory + rel_path = os.path.relpath(ISAACSIM_DIR, UWLAB_DIR) + path_names = ['"${workspaceFolder}/' + rel_path + "/" + path_name + '"' for path_name in path_names] + else: + path_names = [] + print( + f"[WARN] Could not find Isaac Sim VSCode settings: {isaacsim_vscode_filename}." + "\n\tThis will result in missing 'python.analysis.extraPaths' in the VSCode" + "\n\tsettings, which limits the functionality of the Python language server." + "\n\tHowever, it does not affect the functionality of the Isaac Lab project." + "\n\tWe are working on a fix for this issue with the Isaac Sim team." + ) + + # add the path names that are in the Isaac Lab extensions directory + uwlab_extensions = os.listdir(os.path.join(UWLAB_DIR, "source")) + path_names.extend(['"${workspaceFolder}/source/' + ext + '"' for ext in uwlab_extensions]) + + # Add upstream IsaacLab siblings from .../source (editable or wheel) + try: + import isaaclab as _il # noqa: F401 + + pkg_file = pathlib.Path(_il.__file__).resolve() + + # Two possibilities: + # 1) Editable: .../IsaacLab/source/isaaclab/isaaclab/__init__.py β†’ parents[2] == .../source + # 2) Wheel: .../site-packages/isaaclab/__init__.py β†’ parent/"source" == .../isaaclab/source + parent_source = pkg_file.parents[2] + child_source = pkg_file.parent / "source" + + source_dir = child_source if child_source.is_dir() else parent_source + if not source_dir.is_dir(): + raise RuntimeError(f"Could not find 'source' near {pkg_file}") + + for p in sorted(source_dir.iterdir()): + if p.is_dir() and p.name.startswith("isaaclab"): + rel = os.path.relpath(p, UWLAB_DIR).replace("\\", "/") + path_names.append(f'"${{workspaceFolder}}/{rel}"') + except Exception as e: + print(f"[WARN] Could not resolve upstream 'isaaclab' paths: {e}") + + # Append all top-level isaaclab* dirs as ${workspaceFolder}-relative paths + for p in sorted(source_dir.iterdir()): + if p.is_dir() and p.name.startswith("isaaclab"): + rel = os.path.relpath(p, UWLAB_DIR).replace("\\", "/") + path_names.append(f'"${{workspaceFolder}}/{rel}"') + + # combine them into a single string + path_names = ",\n\t\t".expandtabs(4).join(path_names) + # deal with the path separator being different on Windows and Unix + path_names = path_names.replace("\\", "/") + + # replace the path names in the Isaac Lab settings file with the path names parsed + uwlab_settings = re.sub( + r"\"python.analysis.extraPaths\": \[.*?\]", + '"python.analysis.extraPaths": [\n\t\t'.expandtabs(4) + path_names + "\n\t]".expandtabs(4), + uwlab_settings, + flags=re.DOTALL, + ) + # return the Isaac Lab settings string + return uwlab_settings + + +def overwrite_default_python_interpreter(uwlab_settings: str) -> str: + """Overwrite the default python interpreter in the Isaac Lab settings file. + + The default python interpreter is replaced with the path to the python interpreter used by the + isaac-sim project. This is necessary because the default python interpreter is the one shipped with + isaac-sim. + + Args: + uwlab_settings: The settings string to use as template. + + Returns: + The settings string with overwritten default python interpreter. + """ + # read executable name + python_exe = sys.executable.replace("\\", "/") + + # We make an exception for replacing the default interpreter if the + # path (/kit/python/bin/python3) indicates that we are using a local/container + # installation of IsaacSim. We will preserve the calling script as the default, python.sh. + # We want to use python.sh because it modifies LD_LIBRARY_PATH and PYTHONPATH + # (among other envars) that we need for all of our dependencies to be accessible. + if "kit/python/bin/python3" in python_exe: + return uwlab_settings + # replace the default python interpreter in the Isaac Lab settings file with the path to the + # python interpreter in the Isaac Lab directory + uwlab_settings = re.sub( + r"\"python.defaultInterpreterPath\": \".*?\"", + f'"python.defaultInterpreterPath": "{python_exe}"', + uwlab_settings, + flags=re.DOTALL, + ) + # return the Isaac Lab settings file + return uwlab_settings + + +def main(): + # Isaac Lab template settings + uwlab_vscode_template_filename = os.path.join(UWLAB_DIR, ".vscode", "tools", "settings.template.json") + # make sure the Isaac Lab template settings file exists + if not os.path.exists(uwlab_vscode_template_filename): + raise FileNotFoundError( + f"Could not find the Isaac Lab template settings file: {uwlab_vscode_template_filename}" + ) + # read the Isaac Lab template settings file + with open(uwlab_vscode_template_filename) as f: + uwlab_template_settings = f.read() + + # overwrite the python.analysis.extraPaths in the Isaac Lab settings file with the path names + uwlab_settings = overwrite_python_analysis_extra_paths(uwlab_template_settings) + # overwrite the default python interpreter in the Isaac Lab settings file with the path to the + # python interpreter used to call this script + uwlab_settings = overwrite_default_python_interpreter(uwlab_settings) + + # add template notice to the top of the file + header_message = ( + "// This file is a template and is automatically generated by the setup_vscode.py script.\n" + "// Do not edit this file directly.\n" + "// \n" + f"// Generated from: {uwlab_vscode_template_filename}\n" + ) + uwlab_settings = header_message + uwlab_settings + + # write the Isaac Lab settings file + uwlab_vscode_filename = os.path.join(UWLAB_DIR, ".vscode", "settings.json") + with open(uwlab_vscode_filename, "w") as f: + f.write(uwlab_settings) + + # copy the launch.json file if it doesn't exist + uwlab_vscode_launch_filename = os.path.join(UWLAB_DIR, ".vscode", "launch.json") + uwlab_vscode_template_launch_filename = os.path.join(UWLAB_DIR, ".vscode", "tools", "launch.template.json") + if not os.path.exists(uwlab_vscode_launch_filename): + # read template launch settings + with open(uwlab_vscode_template_launch_filename) as f: + uwlab_template_launch_settings = f.read() + # add header + header_message = header_message.replace( + uwlab_vscode_template_filename, uwlab_vscode_template_launch_filename + ) + uwlab_launch_settings = header_message + uwlab_template_launch_settings + # write the Isaac Lab launch settings file + with open(uwlab_vscode_launch_filename, "w") as f: + f.write(uwlab_launch_settings) + + +if __name__ == "__main__": + main() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9aec218 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contribution Guidelines + +UW Lab is a community maintained project. We wholeheartedly welcome contributions to the project to make +the framework more mature and useful for everyone. These may happen in forms of bug reports, feature requests, +design proposals and more. + +For general information on how to contribute see +. + +--- + +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/LICENCE b/LICENCE index 3e687ea..e7d182c 100644 --- a/LICENCE +++ b/LICENCE @@ -8,15 +8,15 @@ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. +this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. +may be used to endorse or promote products derived from this software without +specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED diff --git a/README.md b/README.md index 049689b..503ff65 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,47 @@ - # UW Lab [![IsaacSim](https://img.shields.io/badge/IsaacSim-4.5.0-silver.svg)](https://docs.isaacsim.omniverse.nvidia.com/latest/index.html) -[![IsaacLab](https://img.shields.io/badge/IsaacLab-2.0.2-yellow.svg)](https://github.com/isaac-sim/IsaacLab) [![Python](https://img.shields.io/badge/python-3.10-blue.svg)](https://docs.python.org/3/whatsnew/3.10.html) [![Linux platform](https://img.shields.io/badge/platform-linux--64-orange.svg)](https://releases.ubuntu.com/20.04/) [![Windows platform](https://img.shields.io/badge/platform-windows--64-orange.svg)](https://www.microsoft.com/en-us/) [![pre-commit](https://img.shields.io/github/actions/workflow/status/isaac-sim/IsaacLab/pre-commit.yaml?logo=pre-commit&logoColor=white&label=pre-commit&color=brightgreen)](https://github.com/isaac-sim/IsaacLab/actions/workflows/pre-commit.yaml) [![docs status](https://img.shields.io/github/actions/workflow/status/isaac-sim/IsaacLab/docs.yaml?label=docs&color=brightgreen)](https://github.com/isaac-sim/IsaacLab/actions/workflows/docs.yaml) [![License](https://img.shields.io/badge/license-BSD--3-yellow.svg)](https://opensource.org/licenses/BSD-3-Clause) -[![License](https://img.shields.io/badge/license-Apache--2.0-yellow.svg)](https://opensource.org/license/apache-2-0) - -### Dextrous Manipulation Tasks -[`source/uwlab_tasks/uwlab_tasks/manager_based/manipulation`](https://github.com/UW-Lab/UWLab/tree/main/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation) - - - - - - - - - - - - - - - -
Ur5 w/ Robotiq HandTycho Dextrous Chopsticks ManipulatorX-arm w/ Leap HandFranka with NIST gear mesh task
- -### Advanced Locomotion Skills -[`source/uwlab_tasks/uwlab_tasks/manager_based/locomotion`](https://github.com/UW-Lab/UWLab/tree/main/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion) - -> _Note:_ We focus solely on Boston Dynamics Spot as this is the hardware we currently have set up for sim2real testing. - - - - - - - - - - - - - - - -
Jumping GapsClimbing CliffsPrecision Stepping StonesEscaping Sloped Pits
## Overview -**UW Lab**, is our open source project that builds upon the robust foundation established by Isaac Lab. This effort is organized by the UW (University of Washington) Robotic Students, across labs, and aims to create an centralized repository for high-quality, tested research ready for publication that resides in the IsaacLab Ecosystem. This repo is designed and structured to reuse the toolkit of ongoing IsaacLab development, track a strictly maintained IsaacLab version, and the relevant extensions required for our research on hardware here at UW. +UW Lab builds upon the robust foundation established by Isaac Lab and NVIDIA Isaac Sim, expanding its framework to integrate a wider range of robotics algorithms, platforms, and environments. Rooted in principles of modularity, agility, openness, and a battery-included design inspired from IsaacLab, our framework is crafted to meet the evolving demands of modern robotics research. + +In the short term, our mission is to consolidate and streamline robotics research into one cohesive ecosystem, empowering researchers and developers with a unified platform. Looking ahead, UW Lab envisions a future where artificial intelligence and robotics coalesce seamlessly with physical systemsβ€”bridging the gap between simulation and real-world application. By embedding the laws of physics at its core, our framework provides a realistic, adaptable platform for developing next-generation robotic systems. + +At UW Lab, we believe that the development journey is as significant as the outcome. Our commitment to creating principled, flexible, and extensible structures supports an environment where innovation thrives and every experiment contributes to advancing the field of robotics. Join us as we push the boundaries of what's possible, transforming ideas into tangible, intelligent robotic solutions. + ## Key Features In addition to what IsaacLab provides, UW Lab brings: -- **Environments**: Clean Implementation of tested environments in the ManagerBased format. We both implement our own novel settings and reproduce results from our favorite papers. -- **Sim to Real**: Providing robots and configurations that have been tested in the UW Robotics Labs, demonstrating effective sim2real transfer. +- **Environments**: Cleaned Implementation of reputable environments in Manager-Based format +- **Sim to Real**: Providing robots and configuration that has been tested in Lab and deliver the Simulation Setup that can directly transfer to reals ## Getting Started Our [documentation page](https://uw-lab.github.io/UWLab) provides everything you need to get started, including detailed tutorials and step-by-step guides. Follow these links to learn more about: -- [Installation steps](https://uw-lab.github.io/UWLab/main/source/setup/installation/local_installation.html) +- [Installation steps](https://uw-lab.github.io/UWLab/main/source/setup/installation/index.html) - [Available environments](https://uw-lab.github.io/UWLab/main/source/overview/uw_environments.html) -## Contributing to UW Lab - -Please refer to Isaac Lab -[contribution guideline](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html). - - -## Troubleshooting - -For issues related to Isaac Sim, we recommend checking its [documentation](https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html) -or opening a question on its [forums](https://forums.developer.nvidia.com/c/agx-autonomous-machines/isaac/67). - -or bugs and troubleshooting specific to the UWLab environments, please [submit an issue](https://github.com/UW-Lab/UWLab/issues). - ## Support -* Please use GitHub [Discussions](https://github.com/UW-Lab/UWLab/discussions) for discussing ideas, asking questions, and requests for new features. -* Github [Issues](https://github.com/UW-Lab/UWLab/issues) should only be used to track executable pieces of work with a definite scope and a clear deliverable. These can be fixing bugs, documentation issues, new features, or general updates. +* Please use GitHub [Discussions](https://github.com/uw-lab/UWLab/discussions) for discussing ideas, asking questions, and requests for new features. +* Github [Issues](https://github.com/uw-lab/UWLab/issues) should only be used to track executable pieces of work with a definite scope and a clear deliverable. These can be fixing bugs, documentation issues, new features, or general updates. + ## License -UW Lab is released under [BSD-3 License](LICENSE). The Isaac Lab framework is released under [BSD-3 License](LICENSE). - -## Acknowledgement - -If you found UWLab useful, we appreciate if you cite it in academic publications: -``` -@software{zhang2025uwlab, - author={Zhang, Zhengyu and Yu, Feng and Castro, Mateo and Yin, Patrick and Peng, Quanquan and Scalise, Rosario}, - title={{UWLab}: Environments for robotics research at the edge of reinforcement learning, imitation learning, and sim2real, - year={2025}, - url={https://github.com/UW-Lab/UWLab} -} -``` -UW Lab originated thanks to the ongoing community effort of Isaac Lab. Our repository tracks it closely. As a gratitude we also appreciate if you cite Isaac Lab in academic publications: -``` -@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} -} -``` +UW Lab is released under [BSD-3 License](LICENSE) +The Isaac Lab framework is released under [BSD-3 License](LICENSE). diff --git a/docker/.env.base b/docker/.env.base new file mode 100644 index 0000000..7be5c03 --- /dev/null +++ b/docker/.env.base @@ -0,0 +1,19 @@ +### +# General settings +### + +# Accept the NVIDIA Omniverse EULA by default +ACCEPT_EULA=Y +# NVIDIA Isaac Sim base image +ISAACSIM_BASE_IMAGE=nvcr.io/nvidia/isaac-sim +# NVIDIA Isaac Sim version to use (e.g. 5.1.0) +ISAACSIM_VERSION=5.1.0 +# Derived from the default path in the NVIDIA provided Isaac Sim container +DOCKER_ISAACSIM_ROOT_PATH=/isaac-sim +# The UW Lab path in the container +DOCKER_UWLAB_PATH=/workspace/uwlab +# Docker user directory - by default this is the root user's home directory +DOCKER_USER_HOME=/root +# Docker image and container name suffix (default "", set by the container_interface.py script) +# Example: "-custom" +DOCKER_NAME_SUFFIX="" diff --git a/docker/.env.ros2 b/docker/.env.ros2 new file mode 100644 index 0000000..609704f --- /dev/null +++ b/docker/.env.ros2 @@ -0,0 +1,14 @@ +### +# ROS2 specific settings +### +# Set the version of the ROS2 apt package to install (ros-base, desktop, desktop-full) +ROS2_APT_PACKAGE=ros-base +# Set ROS2 middleware implementation to use (e.g. rmw_fastrtps_cpp, rmw_cyclonedds_cpp) +RMW_IMPLEMENTATION=rmw_fastrtps_cpp +# Path to fastdds.xml file to use (only needed when using fastdds) +FASTRTPS_DEFAULT_PROFILES_FILE=${DOCKER_USER_HOME}/.ros/fastdds.xml +# Path to cyclonedds.xml file to use (only needed when using cyclonedds) +CYCLONEDDS_URI=${DOCKER_USER_HOME}/.ros/cyclonedds.xml +# Docker image and container name suffix (default "", set by the container_interface.py script) +# Example: "-custom" +DOCKER_NAME_SUFFIX="" diff --git a/docker/.ros/cyclonedds.xml b/docker/.ros/cyclonedds.xml new file mode 100644 index 0000000..782c4ae --- /dev/null +++ b/docker/.ros/cyclonedds.xml @@ -0,0 +1,15 @@ + + + + + + + + true + + + auto + 120 + + + diff --git a/docker/.ros/fastdds.xml b/docker/.ros/fastdds.xml new file mode 100644 index 0000000..93ca9c4 --- /dev/null +++ b/docker/.ros/fastdds.xml @@ -0,0 +1,27 @@ + + +Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +NVIDIA CORPORATION and its licensors retain all intellectual property +and proprietary rights in and to this software, related documentation +and any modifications thereto. Any use, reproduction, disclosure or +distribution of this software and related documentation without an express +license agreement from NVIDIA CORPORATION is strictly prohibited. + + + + + + UdpTransport + UDPv4 + + + + + + + UdpTransport + + false + + + diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base new file mode 100644 index 0000000..aca9ec5 --- /dev/null +++ b/docker/Dockerfile.base @@ -0,0 +1,108 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Base image +ARG ISAACSIM_BASE_IMAGE_ARG +ARG ISAACSIM_VERSION_ARG +FROM ${ISAACSIM_BASE_IMAGE_ARG}:${ISAACSIM_VERSION_ARG} AS base +ENV ISAACSIM_VERSION=${ISAACSIM_VERSION_ARG} + +# Set default RUN shell to bash +SHELL ["/bin/bash", "-c"] + +# Adds labels to the Dockerfile +LABEL version="2.1.1" +LABEL description="Dockerfile for building and running the UW Lab framework inside Isaac Sim container image." + +# Arguments +# Path to Isaac Sim root folder +ARG ISAACSIM_ROOT_PATH_ARG +ENV ISAACSIM_ROOT_PATH=${ISAACSIM_ROOT_PATH_ARG} +# Path to the UW Lab directory +ARG UWLAB_PATH_ARG +ENV UWLAB_PATH=${UWLAB_PATH_ARG} +# Home dir of docker user, typically '/root' +ARG DOCKER_USER_HOME_ARG +ENV DOCKER_USER_HOME=${DOCKER_USER_HOME_ARG} + +# Set environment variables +ENV LANG=C.UTF-8 +ENV DEBIAN_FRONTEND=noninteractive + +USER root + +# Install dependencies and remove cache +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + libglib2.0-0 \ + ncurses-term \ + wget && \ + apt -y autoremove && apt clean autoclean && \ + rm -rf /var/lib/apt/lists/* + +# Copy the UW Lab directory (files to exclude are defined in .dockerignore) +COPY ../ ${UWLAB_PATH} + +# Ensure uwlab.sh has execute permissions +RUN chmod +x ${UWLAB_PATH}/uwlab.sh + +# Set up a symbolic link between the installed Isaac Sim root folder and _isaac_sim in the UW Lab directory +RUN ln -sf ${ISAACSIM_ROOT_PATH} ${UWLAB_PATH}/_isaac_sim + +# Install toml dependency +RUN ${UWLAB_PATH}/uwlab.sh -p -m pip install toml + +# Install apt dependencies for extensions that declare them in their extension.toml +RUN --mount=type=cache,target=/var/cache/apt \ + ${UWLAB_PATH}/uwlab.sh -p ${UWLAB_PATH}/tools/install_deps.py apt ${UWLAB_PATH}/source && \ + apt -y autoremove && apt clean autoclean && \ + rm -rf /var/lib/apt/lists/* + +# for singularity usage, have to create the directories that will binded +RUN mkdir -p ${ISAACSIM_ROOT_PATH}/kit/cache && \ + mkdir -p ${DOCKER_USER_HOME}/.cache/ov && \ + mkdir -p ${DOCKER_USER_HOME}/.cache/pip && \ + mkdir -p ${DOCKER_USER_HOME}/.cache/nvidia/GLCache && \ + mkdir -p ${DOCKER_USER_HOME}/.nv/ComputeCache && \ + mkdir -p ${DOCKER_USER_HOME}/.nvidia-omniverse/logs && \ + mkdir -p ${DOCKER_USER_HOME}/.local/share/ov/data && \ + mkdir -p ${DOCKER_USER_HOME}/Documents + +# for singularity usage, create NVIDIA binary placeholders +RUN touch /bin/nvidia-smi && \ + touch /bin/nvidia-debugdump && \ + touch /bin/nvidia-persistenced && \ + touch /bin/nvidia-cuda-mps-control && \ + touch /bin/nvidia-cuda-mps-server && \ + touch /etc/localtime && \ + mkdir -p /var/run/nvidia-persistenced && \ + touch /var/run/nvidia-persistenced/socket + +# installing UW Lab dependencies +# use pip caching to avoid reinstalling large packages +RUN --mount=type=cache,target=${DOCKER_USER_HOME}/.cache/pip \ + ${UWLAB_PATH}/uwlab.sh --install + +# HACK: Remove install of quadprog dependency +RUN ${UWLAB_PATH}/uwlab.sh -p -m pip uninstall -y quadprog + +# aliasing uwlab.sh and python for convenience +RUN echo "export UWLAB_PATH=${UWLAB_PATH}" >> ${HOME}/.bashrc && \ + echo "alias uwlab=${UWLAB_PATH}/uwlab.sh" >> ${HOME}/.bashrc && \ + echo "alias python=${UWLAB_PATH}/_isaac_sim/python.sh" >> ${HOME}/.bashrc && \ + echo "alias python3=${UWLAB_PATH}/_isaac_sim/python.sh" >> ${HOME}/.bashrc && \ + echo "alias pip='${UWLAB_PATH}/_isaac_sim/python.sh -m pip'" >> ${HOME}/.bashrc && \ + echo "alias pip3='${UWLAB_PATH}/_isaac_sim/python.sh -m pip'" >> ${HOME}/.bashrc && \ + echo "alias tensorboard='${UWLAB_PATH}/_isaac_sim/python.sh ${UWLAB_PATH}/_isaac_sim/tensorboard'" >> ${HOME}/.bashrc && \ + echo "export TZ=$(date +%Z)" >> ${HOME}/.bashrc && \ + echo "shopt -s histappend" >> /root/.bashrc && \ + echo "PROMPT_COMMAND='history -a'" >> /root/.bashrc + +# make working directory as the UW Lab directory +# this is the default directory when the container is run +WORKDIR ${UWLAB_PATH} diff --git a/docker/Dockerfile.ros2 b/docker/Dockerfile.ros2 new file mode 100644 index 0000000..88c8e7c --- /dev/null +++ b/docker/Dockerfile.ros2 @@ -0,0 +1,39 @@ +# Everything past this stage is to install +# ROS2 Humble + +# What is the docker name suffix for the base image to load? (defaults to empty string) +ARG DOCKER_NAME_SUFFIX="" + +FROM uw-lab-base${DOCKER_NAME_SUFFIX} AS ros2 + +# Which ROS2 apt package to install +ARG ROS2_APT_PACKAGE + +# ROS2 Humble Apt installations +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && apt-get install -y --no-install-recommends \ + curl \ + # Install ROS2 Humble \ + software-properties-common && \ + add-apt-repository universe && \ + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo jammy) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null && \ + apt-get update && apt-get install -y --no-install-recommends \ + ros-humble-${ROS2_APT_PACKAGE} \ + ros-humble-vision-msgs \ + # Install both FastRTPS and CycloneDDS + ros-humble-rmw-cyclonedds-cpp \ + ros-humble-rmw-fastrtps-cpp \ + # This includes various dev tools including colcon + ros-dev-tools && \ + # Install rosdeps for extensions that declare a ros_ws in + # their extension.toml + ${UWLAB_PATH}/uwlab.sh -p ${UWLAB_PATH}/tools/install_deps.py rosdep ${UWLAB_PATH}/source && \ + apt -y autoremove && apt clean autoclean && \ + rm -rf /var/lib/apt/lists/* && \ + # Add sourcing of setup.bash to .bashrc + echo "source /opt/ros/humble/setup.bash" >> ${HOME}/.bashrc + +# Copy the RMW specifications for ROS2 +# https://docs.isaacsim.omniverse.nvidia.com/latest/installation/install_ros.html +COPY docker/.ros/ ${DOCKER_USER_HOME}/.ros/ diff --git a/docker/cluster/.env.cluster b/docker/cluster/.env.cluster new file mode 100644 index 0000000..9e3e939 --- /dev/null +++ b/docker/cluster/.env.cluster @@ -0,0 +1,22 @@ +### +# Cluster specific settings +### + +# Job scheduler used by cluster. +# Currently supports PBS and SLURM +CLUSTER_JOB_SCHEDULER=SLURM +# Docker cache dir for Isaac Sim (has to end on docker-isaac-sim) +# e.g. /cluster/scratch/$USER/docker-isaac-sim +CLUSTER_ISAAC_SIM_CACHE_DIR=/some/path/on/cluster/docker-isaac-sim +# UW Lab directory on the cluster (has to end on uwlab) +# e.g. /cluster/home/$USER/uwlab +CLUSTER_UWLAB_DIR=/some/path/on/cluster/uwlab +# Cluster login +CLUSTER_LOGIN=username@cluster_ip +# Cluster scratch directory to store the SIF file +# e.g. /cluster/scratch/$USER +CLUSTER_SIF_PATH=/some/path/on/cluster/ +# Remove the temporary uwlab code copy after the job is done +REMOVE_CODE_COPY_AFTER_JOB=false +# Python executable within UW Lab directory to run with the submitted job +CLUSTER_PYTHON_EXECUTABLE=scripts/reinforcement_learning/rsl_rl/train.py diff --git a/docker/cluster/cluster_interface.sh b/docker/cluster/cluster_interface.sh new file mode 100755 index 0000000..dfca11d --- /dev/null +++ b/docker/cluster/cluster_interface.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash + +#== +# Configurations +#== + +# Exits if error occurs +set -e + +# Set tab-spaces +tabs 4 + +# get script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +#== +# Functions +#== +# Function to display warnings in red +display_warning() { + echo -e "\033[31mWARNING: $1\033[0m" +} + +# Helper function to compare version numbers +version_gte() { + # Returns 0 if the first version is greater than or equal to the second, otherwise 1 + [ "$(printf '%s\n' "$1" "$2" | sort -V | head -n 1)" == "$2" ] +} + +# Function to check docker versions +check_docker_version() { + # check if docker is installed + if ! command -v docker &> /dev/null; then + echo "[Error] Docker is not installed! Please check the 'Docker Guide' for instruction." >&2; + exit 1 + fi + # Retrieve Docker version + docker_version=$(docker --version | awk '{ print $3 }') + apptainer_version=$(apptainer --version | awk '{ print $3 }') + + # Check if Docker version is exactly 24.0.7 or Apptainer version is exactly 1.2.5 + if [ "$docker_version" = "24.0.7" ] && [ "$apptainer_version" = "1.2.5" ]; then + echo "[INFO]: Docker version ${docker_version} and Apptainer version ${apptainer_version} are tested and compatible." + + # Check if Docker version is >= 27.0.0 and Apptainer version is >= 1.3.4 + elif version_gte "$docker_version" "27.0.0" && version_gte "$apptainer_version" "1.3.4"; then + echo "[INFO]: Docker version ${docker_version} and Apptainer version ${apptainer_version} are tested and compatible." + + # Else, display a warning for non-tested versions + else + display_warning "Docker version ${docker_version} and Apptainer version ${apptainer_version} are non-tested versions. There could be issues, please try to update them. More info: https://uw-lab.github.io/UWLab/source/deployment/cluster.html" + fi +} + +# Checks if a docker image exists, otherwise prints warning and exists +check_image_exists() { + image_name="$1" + if ! docker image inspect $image_name &> /dev/null; then + echo "[Error] The '$image_name' image does not exist!" >&2; + echo "[Error] You might be able to build it with /UWLab/docker/container.py." >&2; + exit 1 + fi +} + +# Check if the singularity image exists on the remote host, otherwise print warning and exit +check_singularity_image_exists() { + image_name="$1" + if ! ssh "$CLUSTER_LOGIN" "[ -f $CLUSTER_SIF_PATH/$image_name.tar ]"; then + echo "[Error] The '$image_name' image does not exist on the remote host $CLUSTER_LOGIN!" >&2; + exit 1 + fi +} + +submit_job() { + + echo "[INFO] Arguments passed to job script ${@}" + + case $CLUSTER_JOB_SCHEDULER in + "SLURM") + job_script_file=submit_job_slurm.sh + ;; + "PBS") + job_script_file=submit_job_pbs.sh + ;; + *) + echo "[ERROR] Unsupported job scheduler specified: '$CLUSTER_JOB_SCHEDULER'. Supported options are: ['SLURM', 'PBS']" + exit 1 + ;; + esac + + ssh $CLUSTER_LOGIN "cd $CLUSTER_UWLAB_DIR && bash $CLUSTER_UWLAB_DIR/docker/cluster/$job_script_file \"$CLUSTER_UWLAB_DIR\" \"uw-lab-$profile\" ${@}" +} + +#== +# Main +#== + +#!/bin/bash + +help() { + echo -e "\nusage: $(basename "$0") [-h] [] [...] -- Utility for interfacing between UWLab and compute clusters." + echo -e "\noptions:" + echo -e " -h Display this help message." + echo -e "\ncommands:" + echo -e " push [] Push the docker image to the cluster." + echo -e " job [] [] Submit a job to the cluster." + echo -e "\nwhere:" + echo -e " is the optional container profile specification. Defaults to 'base'." + echo -e " are optional arguments specific to the job command." + echo -e "\n" >&2 +} + +# Parse options +while getopts ":h" opt; do + case ${opt} in + h ) + help + exit 0 + ;; + \? ) + echo "Invalid option: -$OPTARG" >&2 + help + exit 1 + ;; + esac +done +shift $((OPTIND -1)) + +# Check for command +if [ $# -lt 1 ]; then + echo "Error: Command is required." >&2 + help + exit 1 +fi + +command=$1 +shift +profile="base" + +case $command in + push) + if [ $# -gt 1 ]; then + echo "Error: Too many arguments for push command." >&2 + help + exit 1 + fi + [ $# -eq 1 ] && profile=$1 + echo "Executing push command" + [ -n "$profile" ] && echo "Using profile: $profile" + if ! command -v apptainer &> /dev/null; then + echo "[INFO] Exiting because apptainer was not installed" + echo "[INFO] You may follow the installation procedure from here: https://apptainer.org/docs/admin/main/installation.html#install-ubuntu-packages" + exit + fi + # Check if Docker image exists + check_image_exists uw-lab-$profile:latest + # Check docker and apptainer version + check_docker_version + # source env file to get cluster login and path information + source $SCRIPT_DIR/.env.cluster + # make sure exports directory exists + mkdir -p /$SCRIPT_DIR/exports + # clear old exports for selected profile + rm -rf /$SCRIPT_DIR/exports/uw-lab-$profile* + # create singularity image + # NOTE: we create the singularity image as non-root user to allow for more flexibility. If this causes + # issues, remove the --fakeroot flag and open an issue on the UWLab repository. + cd /$SCRIPT_DIR/exports + APPTAINER_NOHTTPS=1 apptainer build --sandbox --fakeroot uw-lab-$profile.sif docker-daemon://uw-lab-$profile:latest + # tar image (faster to send single file as opposed to directory with many files) + tar -cvf /$SCRIPT_DIR/exports/uw-lab-$profile.tar uw-lab-$profile.sif + # make sure target directory exists + ssh $CLUSTER_LOGIN "mkdir -p $CLUSTER_SIF_PATH" + # send image to cluster + scp $SCRIPT_DIR/exports/uw-lab-$profile.tar $CLUSTER_LOGIN:$CLUSTER_SIF_PATH/uw-lab-$profile.tar + ;; + job) + if [ $# -ge 1 ]; then + passed_profile=$1 + if [ -f "$SCRIPT_DIR/../.env.$passed_profile" ]; then + profile=$passed_profile + shift + fi + fi + job_args="$@" + echo "[INFO] Executing job command" + [ -n "$profile" ] && echo -e "\tUsing profile: $profile" + [ -n "$job_args" ] && echo -e "\tJob arguments: $job_args" + source $SCRIPT_DIR/.env.cluster + # Get current date and time + current_datetime=$(date +"%Y%m%d_%H%M%S") + # Append current date and time to CLUSTER_UWLAB_DIR + CLUSTER_UWLAB_DIR="${CLUSTER_UWLAB_DIR}_${current_datetime}" + # Check if singularity image exists on the remote host + check_singularity_image_exists uw-lab-$profile + # make sure target directory exists + ssh $CLUSTER_LOGIN "mkdir -p $CLUSTER_UWLAB_DIR" + # Sync UW Lab code + echo "[INFO] Syncing UW Lab code..." + rsync -rh --exclude="*.git*" --filter=':- .dockerignore' /$SCRIPT_DIR/../.. $CLUSTER_LOGIN:$CLUSTER_UWLAB_DIR + # execute job script + echo "[INFO] Executing job script..." + # check whether the second argument is a profile or a job argument + submit_job $job_args + ;; + *) + echo "Error: Invalid command: $command" >&2 + help + exit 1 + ;; +esac diff --git a/docker/cluster/run_singularity.sh b/docker/cluster/run_singularity.sh new file mode 100755 index 0000000..1795113 --- /dev/null +++ b/docker/cluster/run_singularity.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +echo "(run_singularity.py): Called on compute node from current uwlab directory $1 with container profile $2 and arguments ${@:3}" + +#== +# Helper functions +#== + +setup_directories() { + # Check and create directories + for dir in \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/cache/kit" \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/cache/ov" \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/cache/pip" \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/cache/glcache" \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/cache/computecache" \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/logs" \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/data" \ + "${CLUSTER_ISAAC_SIM_CACHE_DIR}/documents"; do + if [ ! -d "$dir" ]; then + mkdir -p "$dir" + echo "Created directory: $dir" + fi + done +} + + +#== +# Main +#== + + +# get script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +# load variables to set the UW Lab path on the cluster +source $SCRIPT_DIR/.env.cluster +source $SCRIPT_DIR/../.env.base + +# make sure that all directories exists in cache directory +setup_directories +# copy all cache files +cp -r $CLUSTER_ISAAC_SIM_CACHE_DIR $TMPDIR + +# make sure logs directory exists (in the permanent uwlab directory) +mkdir -p "$CLUSTER_UWLAB_DIR/logs" +touch "$CLUSTER_UWLAB_DIR/logs/.keep" + +# copy the temporary uwlab directory with the latest changes to the compute node +cp -r $1 $TMPDIR +# Get the directory name +dir_name=$(basename "$1") + +# copy container to the compute node +tar -xf $CLUSTER_SIF_PATH/$2.tar -C $TMPDIR + +# execute command in singularity container +# NOTE: UWLAB_PATH is normally set in `uwlab.sh` but we directly call the isaac-sim python because we sync the entire +# UW Lab directory to the compute node and remote the symbolic link to isaac-sim +singularity exec \ + -B $TMPDIR/docker-isaac-sim/cache/kit:${DOCKER_ISAACSIM_ROOT_PATH}/kit/cache:rw \ + -B $TMPDIR/docker-isaac-sim/cache/ov:${DOCKER_USER_HOME}/.cache/ov:rw \ + -B $TMPDIR/docker-isaac-sim/cache/pip:${DOCKER_USER_HOME}/.cache/pip:rw \ + -B $TMPDIR/docker-isaac-sim/cache/glcache:${DOCKER_USER_HOME}/.cache/nvidia/GLCache:rw \ + -B $TMPDIR/docker-isaac-sim/cache/computecache:${DOCKER_USER_HOME}/.nv/ComputeCache:rw \ + -B $TMPDIR/docker-isaac-sim/logs:${DOCKER_USER_HOME}/.nvidia-omniverse/logs:rw \ + -B $TMPDIR/docker-isaac-sim/data:${DOCKER_USER_HOME}/.local/share/ov/data:rw \ + -B $TMPDIR/docker-isaac-sim/documents:${DOCKER_USER_HOME}/Documents:rw \ + -B $TMPDIR/$dir_name:/workspace/uwlab:rw \ + -B $CLUSTER_UWLAB_DIR/logs:/workspace/uwlab/logs:rw \ + --nv --writable --containall $TMPDIR/$2.sif \ + bash -c "export UWLAB_PATH=/workspace/uwlab && cd /workspace/uwlab && /isaac-sim/python.sh ${CLUSTER_PYTHON_EXECUTABLE} ${@:3}" + +# copy resulting cache files back to host +rsync -azPv $TMPDIR/docker-isaac-sim $CLUSTER_ISAAC_SIM_CACHE_DIR/.. + +# if defined, remove the temporary uwlab directory pushed when the job was submitted +if $REMOVE_CODE_COPY_AFTER_JOB; then + rm -rf $1 +fi + +echo "(run_singularity.py): Return" diff --git a/docker/cluster/submit_job_pbs.sh b/docker/cluster/submit_job_pbs.sh new file mode 100755 index 0000000..aaf7718 --- /dev/null +++ b/docker/cluster/submit_job_pbs.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# in the case you need to load specific modules on the cluster, add them here +# e.g., `module load eth_proxy` + +# create job script with compute demands +### MODIFY HERE FOR YOUR JOB ### +cat < job.sh +#!/bin/bash + +#PBS -l select=1:ncpus=8:mpiprocs=1:ngpus=1 +#PBS -l walltime=01:00:00 +#PBS -j oe +#PBS -q gpu +#PBS -N uwlab +#PBS -m bea -M "user@mail" + +# Pass the container profile first to run_singularity.sh, then all arguments intended for the executed script +bash "$1/docker/cluster/run_singularity.sh" "$1" "$2" "${@:3}" +EOT + +qsub job.sh +rm job.sh diff --git a/docker/cluster/submit_job_slurm.sh b/docker/cluster/submit_job_slurm.sh new file mode 100755 index 0000000..22c216d --- /dev/null +++ b/docker/cluster/submit_job_slurm.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# in the case you need to load specific modules on the cluster, add them here +# e.g., `module load eth_proxy` + +# create job script with compute demands +### MODIFY HERE FOR YOUR JOB ### +cat < job.sh +#!/bin/bash + +#SBATCH -n 1 +#SBATCH --cpus-per-task=8 +#SBATCH --gpus=rtx_3090:1 +#SBATCH --time=23:00:00 +#SBATCH --mem-per-cpu=4048 +#SBATCH --mail-type=END +#SBATCH --mail-user=name@mail +#SBATCH --job-name="training-$(date +"%Y-%m-%dT%H:%M")" + +# Pass the container profile first to run_singularity.sh, then all arguments intended for the executed script +bash "$1/docker/cluster/run_singularity.sh" "$1" "$2" "${@:3}" +EOT + +sbatch < job.sh +rm job.sh diff --git a/docker/container.py b/docker/container.py new file mode 100755 index 0000000..d161186 --- /dev/null +++ b/docker/container.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import argparse +import shutil +from pathlib import Path + +from utils import ContainerInterface, x11_utils + + +def parse_cli_args() -> argparse.Namespace: + """Parse command line arguments. + + This function creates a parser object and adds subparsers for each command. The function then parses the + command line arguments and returns the parsed arguments. + + Returns: + The parsed command line arguments. + """ + parser = argparse.ArgumentParser(description="Utility for using Docker with UW Lab.") + + # We have to create separate parent parsers for common options to our subparsers + parent_parser = argparse.ArgumentParser(add_help=False) + parent_parser.add_argument( + "profile", nargs="?", default="base", help="Optional container profile specification. Example: 'base' or 'ros'." + ) + parent_parser.add_argument( + "--files", + nargs="*", + default=None, + help=( + "Allows additional '.yaml' files to be passed to the docker compose command. These files will be merged" + " with 'docker-compose.yaml' in their provided order." + ), + ) + parent_parser.add_argument( + "--env-files", + nargs="*", + default=None, + help=( + "Allows additional '.env' files to be passed to the docker compose command. These files will be merged with" + " '.env.base' in their provided order." + ), + ) + parent_parser.add_argument( + "--suffix", + nargs="?", + default=None, + help=( + "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 ``uw-lab-base-custom``." + ), + ) + + # Actual command definition begins here + subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser( + "start", + help="Build the docker image and create the container in detached mode.", + parents=[parent_parser], + ) + subparsers.add_parser( + "enter", help="Begin a new bash process within an existing UW Lab container.", parents=[parent_parser] + ) + config = subparsers.add_parser( + "config", + help=( + "Generate a docker-compose.yaml from the passed yamls, .envs, and either print to the terminal or create a" + " yaml at output_yaml" + ), + parents=[parent_parser], + ) + config.add_argument( + "--output-yaml", nargs="?", default=None, help="Yaml file to write config output to. Defaults to None." + ) + subparsers.add_parser( + "copy", help="Copy build and logs artifacts from the container to the host machine.", parents=[parent_parser] + ) + subparsers.add_parser("stop", help="Stop the docker container and remove it.", parents=[parent_parser]) + + # parse the arguments to determine the command + args = parser.parse_args() + + return args + + +def main(args: argparse.Namespace): + """Main function for the Docker utility.""" + # check if docker is installed + if not shutil.which("docker"): + raise RuntimeError( + "Docker is not installed! Please check the 'Docker Guide' for instruction: " + "https://uw-lab.github.io/UWLab/source/deployment/docker.html" + ) + + # creating container interface + ci = ContainerInterface( + context_dir=Path(__file__).resolve().parent, + profile=args.profile, + yamls=args.files, + envs=args.env_files, + suffix=args.suffix, + ) + + print(f"[INFO] Using container profile: {ci.profile}") + if 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 + if x11_outputs is not None: + (x11_yaml, x11_envar) = x11_outputs + ci.add_yamls += x11_yaml + ci.environ.update(x11_envar) + # start the container + ci.start() + elif args.command == "enter": + # refresh the x11 forwarding + x11_utils.x11_refresh(ci.statefile) + # enter the container + ci.enter() + elif args.command == "config": + ci.config(args.output_yaml) + elif args.command == "copy": + ci.copy() + elif args.command == "stop": + # stop the container + ci.stop() + # cleanup the x11 forwarding + x11_utils.x11_cleanup(ci.statefile) + else: + raise RuntimeError(f"Invalid command provided: {args.command}. Please check the help message.") + + +if __name__ == "__main__": + args_cli = parse_cli_args() + main(args_cli) diff --git a/docker/container.sh b/docker/container.sh new file mode 100755 index 0000000..ecd3d83 --- /dev/null +++ b/docker/container.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# print warning of deprecated script in yellow +echo -e "\e[33m------------------------------------------------------------" +echo -e "WARNING: This script is deprecated and will be removed in the future. Please use 'docker/container.py' instead." +echo -e "------------------------------------------------------------\e[0m\n" + +# obtain current directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +# call the python script +python3 "${SCRIPT_DIR}/container.py" "${@:1}" diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..c855bc5 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,153 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Here we set the parts that would +# be re-used between services to an +# extension field +# https://docs.docker.com/compose/compose-file/compose-file-v3/#extension-fields +x-default-uw-lab-volumes: &default-uw-lab-volumes + # These volumes follow from this page + # https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/install_faq.html#save-isaac-sim-configs-on-local-disk + - type: volume + source: isaac-cache-kit + target: ${DOCKER_ISAACSIM_ROOT_PATH}/kit/cache + - type: volume + source: isaac-cache-ov + target: ${DOCKER_USER_HOME}/.cache/ov + - type: volume + source: isaac-cache-pip + target: ${DOCKER_USER_HOME}/.cache/pip + - type: volume + source: isaac-cache-gl + target: ${DOCKER_USER_HOME}/.cache/nvidia/GLCache + - type: volume + source: isaac-cache-compute + target: ${DOCKER_USER_HOME}/.nv/ComputeCache + - type: volume + source: isaac-logs + target: ${DOCKER_USER_HOME}/.nvidia-omniverse/logs + - type: volume + source: isaac-carb-logs + target: ${DOCKER_ISAACSIM_ROOT_PATH}/kit/logs/Kit/Isaac-Sim + - type: volume + source: isaac-data + target: ${DOCKER_USER_HOME}/.local/share/ov/data + - type: volume + source: isaac-docs + target: ${DOCKER_USER_HOME}/Documents + # This overlay allows changes on the local files to + # be reflected within the container immediately + - type: bind + source: ../source + target: ${DOCKER_UWLAB_PATH}/source + - type: bind + source: ../scripts + target: ${DOCKER_UWLAB_PATH}/scripts + - type: bind + source: ../docs + target: ${DOCKER_UWLAB_PATH}/docs + - type: bind + source: ../tools + target: ${DOCKER_UWLAB_PATH}/tools + # The effect of these volumes is twofold: + # 1. Prevent root-owned files from flooding the _build and logs dir + # on the host machine + # 2. Preserve the artifacts in persistent volumes for later copying + # to the host machine + - type: volume + source: uw-lab-docs + target: ${DOCKER_UWLAB_PATH}/docs/_build + - type: volume + source: uw-lab-logs + target: ${DOCKER_UWLAB_PATH}/logs + - type: volume + source: uw-lab-data + target: ${DOCKER_UWLAB_PATH}/data_storage + # This volume is used to store the history of the bash shell + - type: bind + source: .uw-lab-docker-history + target: ${DOCKER_USER_HOME}/.bash_history + +x-default-uw-lab-environment: &default-uw-lab-environment + - ISAACSIM_PATH=${DOCKER_UWLAB_PATH}/_isaac_sim + - OMNI_KIT_ALLOW_ROOT=1 + +x-default-uw-lab-deploy: &default-uw-lab-deploy + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [ gpu ] + +services: + # This service is the base UW Lab image + uw-lab-base: + profiles: [ "base" ] + env_file: .env.base + build: + context: ../ + dockerfile: docker/Dockerfile.base + args: + - ISAACSIM_BASE_IMAGE_ARG=${ISAACSIM_BASE_IMAGE} + - ISAACSIM_VERSION_ARG=${ISAACSIM_VERSION} + - ISAACSIM_ROOT_PATH_ARG=${DOCKER_ISAACSIM_ROOT_PATH} + - UWLAB_PATH_ARG=${DOCKER_UWLAB_PATH} + - DOCKER_USER_HOME_ARG=${DOCKER_USER_HOME} + image: uw-lab-base${DOCKER_NAME_SUFFIX-} + container_name: uw-lab-base${DOCKER_NAME_SUFFIX-} + environment: *default-uw-lab-environment + volumes: *default-uw-lab-volumes + network_mode: host + deploy: *default-uw-lab-deploy + # This is the entrypoint for the container + entrypoint: bash + stdin_open: true + tty: true + + # This service adds a ROS2 Humble + # installation on top of the base image + uw-lab-ros2: + profiles: [ "ros2" ] + env_file: + - .env.base + - .env.ros2 + build: + context: ../ + dockerfile: docker/Dockerfile.ros2 + args: + # ROS2_APT_PACKAGE will default to NONE. This is to + # avoid a warning message when building only the base profile + # with the .env.base file + - ROS2_APT_PACKAGE=${ROS2_APT_PACKAGE:-NONE} + # Make sure that the correct Docker Name Suffix is being passed to the dockerfile, to know which base image to + # start from. + - DOCKER_NAME_SUFFIX=${DOCKER_NAME_SUFFIX-} + image: uw-lab-ros2${DOCKER_NAME_SUFFIX-} + container_name: uw-lab-ros2${DOCKER_NAME_SUFFIX-} + environment: *default-uw-lab-environment + volumes: *default-uw-lab-volumes + network_mode: host + deploy: *default-uw-lab-deploy + # This is the entrypoint for the container + entrypoint: bash + stdin_open: true + tty: true + +volumes: + # isaac-sim + isaac-cache-kit: + isaac-cache-ov: + isaac-cache-pip: + isaac-cache-gl: + isaac-cache-compute: + isaac-logs: + isaac-carb-logs: + isaac-data: + isaac-docs: + # uw-lab + uw-lab-docs: + uw-lab-logs: + uw-lab-data: diff --git a/docker/test/test_docker.py b/docker/test/test_docker.py new file mode 100644 index 0000000..399475f --- /dev/null +++ b/docker/test/test_docker.py @@ -0,0 +1,64 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +import subprocess +from pathlib import Path + +import pytest + + +def start_stop_docker(profile, suffix): + """Test starting and stopping docker profile with suffix.""" + environ = os.environ + context_dir = Path(__file__).resolve().parent.parent + + # generate parameters for the arguments + if suffix != "": + container_name = f"uw-lab-{profile}-{suffix}" + suffix_args = ["--suffix", suffix] + else: + container_name = f"uw-lab-{profile}" + suffix_args = [] + + run_kwargs = { + "check": False, + "capture_output": True, + "text": True, + "cwd": context_dir, + "env": environ, + } + + # start the container + docker_start = subprocess.run(["python", "container.py", "start", profile] + suffix_args, **run_kwargs) + assert docker_start.returncode == 0 + + # verify that the container is running + docker_running_true = subprocess.run(["docker", "ps"], **run_kwargs) + assert docker_running_true.returncode == 0 + assert container_name in docker_running_true.stdout + + # stop the container + docker_stop = subprocess.run(["python", "container.py", "stop", profile] + suffix_args, **run_kwargs) + assert docker_stop.returncode == 0 + + # verify that the container has stopped + docker_running_false = subprocess.run(["docker", "ps"], **run_kwargs) + assert docker_running_false.returncode == 0 + assert container_name not in docker_running_false.stdout + + +@pytest.mark.parametrize( + "profile,suffix", + [ + ("base", ""), + ("base", "test"), + ("ros2", ""), + ("ros2", "test"), + ], +) +def test_docker_profiles(profile, suffix): + """Test starting and stopping docker profiles with and without suffixes.""" + start_stop_docker(profile, suffix) diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py new file mode 100644 index 0000000..8635109 --- /dev/null +++ b/docker/utils/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from .container_interface import ContainerInterface + +__all__ = ["ContainerInterface"] diff --git a/docker/utils/container_interface.py b/docker/utils/container_interface.py new file mode 100644 index 0000000..1d38b4e --- /dev/null +++ b/docker/utils/container_interface.py @@ -0,0 +1,316 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path +from typing import Any + +from .state_file import StateFile + + +class ContainerInterface: + """A helper class for managing UW Lab containers.""" + + def __init__( + self, + context_dir: Path, + profile: str = "base", + yamls: list[str] | None = None, + envs: list[str] | None = None, + statefile: StateFile | None = None, + suffix: str | None = None, + ): + """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 + 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 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 ``uw-lab-base-custom``. + """ + # set the context directory + self.context_dir = context_dir + + # create a state-file if not provided + # the state file is a manager of run-time state variables that are saved to a file + if statefile is None: + self.statefile = StateFile(path=self.context_dir / ".container.cfg") + else: + self.statefile = statefile + + # set the profile and container name + self.profile = profile + if self.profile == "uwlab": + # Silently correct from uwlab to base, because uwlab is a commonly passed arg + # but not a real profile + self.profile = "base" + + # set the docker image and container name suffix + if suffix is None or suffix == "": + # if no name suffix is given, default to the empty string as the name suffix + self.suffix = "" + else: + # insert a hyphen before the suffix if a suffix is given + self.suffix = f"-{suffix}" + + self.container_name = f"uw-lab-{self.profile}{self.suffix}" + self.image_name = f"uw-lab-{self.profile}{self.suffix}:latest" + + # keep the environment variables from the current environment, + # except make sure that the docker name suffix is set from the script + self.environ = os.environ.copy() + self.environ["DOCKER_NAME_SUFFIX"] = self.suffix + + # resolve the image extension through the passed yamls and envs + self._resolve_image_extension(yamls, envs) + # load the environment variables from the .env files + self._parse_dot_vars() + + """ + Operations. + """ + + def is_container_running(self) -> bool: + """Check if the container is running. + + Returns: + True if the container is running, otherwise False. + """ + status = subprocess.run( + ["docker", "container", "inspect", "-f", "{{.State.Status}}", self.container_name], + capture_output=True, + text=True, + check=False, + ).stdout.strip() + return status == "running" + + def does_image_exist(self) -> bool: + """Check if the Docker image exists. + + Returns: + True if the image exists, otherwise False. + """ + result = subprocess.run(["docker", "image", "inspect", self.image_name], capture_output=True, text=True) + return result.returncode == 0 + + def start(self): + """Build and start the Docker container using the Docker compose command.""" + print( + f"[INFO] Building the docker image and starting the container '{self.container_name}' in the" + " background...\n" + ) + # Check if the container history file exists + container_history_file = self.context_dir / ".uw-lab-docker-history" + if not container_history_file.exists(): + # Create the file with sticky bit on the group + container_history_file.touch(mode=0o2644, exist_ok=True) + + # 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", + "uw-lab-base", + ], + check=False, + cwd=self.context_dir, + env=self.environ, + ) + + # build the image for the profile + subprocess.run( + ["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, + ) + + def enter(self): + """Enter the running container by executing a bash shell. + + Raises: + RuntimeError: If the container is not running. + """ + 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", + ]) + 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. + """ + 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, + ) + else: + raise RuntimeError(f"Can't stop container '{self.container_name}' as it is not running.") + + def copy(self, output_dir: Path | None = None): + """Copy artifacts from the running container to the host machine. + + Args: + output_dir: The directory to copy the artifacts to. Defaults to None, in which case + the context directory is used. + + Raises: + RuntimeError: If the container is not running. + """ + if self.is_container_running(): + print(f"[INFO] Copying artifacts from the '{self.container_name}' container...\n") + if output_dir is None: + output_dir = self.context_dir + + # create a directory to store the artifacts + output_dir = output_dir.joinpath("artifacts") + if not output_dir.is_dir(): + output_dir.mkdir() + + # define dictionary of mapping from docker container path to host machine path + docker_isaac_lab_path = Path(self.dot_vars["DOCKER_UWLAB_PATH"]) + artifacts = { + docker_isaac_lab_path.joinpath("logs"): output_dir.joinpath("logs"), + docker_isaac_lab_path.joinpath("docs/_build"): output_dir.joinpath("docs"), + docker_isaac_lab_path.joinpath("data_storage"): output_dir.joinpath("data_storage"), + } + # print the artifacts to be copied + for container_path, host_path in artifacts.items(): + print(f"\t -{container_path} -> {host_path}") + # remove the existing artifacts + for path in artifacts.values(): + shutil.rmtree(path, ignore_errors=True) + + # copy the artifacts + for container_path, host_path in artifacts.items(): + subprocess.run( + [ + "docker", + "cp", + f"uw-lab-{self.profile}{self.suffix}:{container_path}/", + f"{host_path}", + ], + check=False, + ) + print("\n[INFO] Finished copying the artifacts from the container.") + else: + raise RuntimeError(f"The container '{self.container_name}' is not running.") + + def config(self, output_yaml: Path | None = None): + """Process the Docker compose configuration based on the passed yamls and environment files. + + If the :attr:`output_yaml` is not None, the configuration is written to the file. Otherwise, it is printed to + the terminal. + + Args: + output_yaml: The path to the yaml file where the configuration is written to. Defaults + to None, in which case the configuration is printed to the terminal. + """ + print("[INFO] Configuring the passed options into a yaml...\n") + + # resolve the output argument + if output_yaml is not None: + output = ["--output", output_yaml] + else: + 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, + ) + + """ + 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. + + Args: + 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. + """ + self.add_yamls = ["--file", "docker-compose.yaml"] + self.add_profiles = ["--profile", f"{self.profile}"] + self.add_env_files = ["--env-file", ".env.base"] + + # extend env file based on profile + if self.profile != "base": + self.add_env_files += ["--env-file", f".env.{self.profile}"] + + # extend the env file based on the passed envs + if envs is not None: + for env in envs: + self.add_env_files += ["--env-file", env] + + # extend the docker-compose.yaml based on the passed yamls + if yamls is not None: + for yaml in yamls: + self.add_yamls += ["--file", yaml] + + def _parse_dot_vars(self): + """Parse the environment variables from the .env files. + + Based on the passed ".env" files, this function reads the environment variables and stores them in a dictionary. + The environment variables are read in order and overwritten if there are name conflicts, mimicking the behavior + of Docker compose. + """ + self.dot_vars: dict[str, Any] = {} + + # check if the number of arguments is even for the env files + if len(self.add_env_files) % 2 != 0: + raise RuntimeError( + "The parameters for env files are configured incorrectly. There should be an even number of arguments." + f" Received: {self.add_env_files}." + ) + + # read the environment variables from the .env files + for i in range(1, len(self.add_env_files), 2): + with open(self.context_dir / self.add_env_files[i]) as f: + self.dot_vars.update(dict(line.strip().split("=", 1) for line in f if "=" in line)) diff --git a/docker/utils/state_file.py b/docker/utils/state_file.py new file mode 100644 index 0000000..5e54b9d --- /dev/null +++ b/docker/utils/state_file.py @@ -0,0 +1,151 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import configparser +from configparser import ConfigParser +from pathlib import Path +from typing import Any + + +class StateFile: + """A class to manage state variables parsed from a configuration file. + + This class provides a simple interface to set, get, and delete variables from a configuration + object. It also provides the ability to save the configuration object to a file. + + It thinly wraps around the ConfigParser class from the configparser module. + """ + + def __init__(self, path: Path, namespace: str | None = None): + """Initialize the class instance and load the configuration file. + + Args: + path: The path to the configuration file. + namespace: The default namespace to use when setting and getting variables. + Namespace corresponds to a section in the configuration file. Defaults to None, + meaning all member functions will have to specify the section explicitly, + or :attr:`StateFile.namespace` must be set manually. + """ + self.path = path + self.namespace = namespace + + # load the configuration file + self.load() + + def __del__(self): + """ + Save the loaded configuration to the initial file path upon deconstruction. This helps + ensure that the configuration file is always up to date. + """ + # save the configuration file + self.save() + + """ + Operations. + """ + + def set_variable(self, key: str, value: Any, section: str | None = None): + """Set a variable into the configuration object. + + Note: + Since we use the ConfigParser class, the section names are case-sensitive but the keys are not. + + Args: + key: The key of the variable to be set. + value: The value of the variable to be set. + section: The section of the configuration object to set the variable in. + Defaults to None, in which case the default section is used. + + Raises: + configparser.Error: If no section is specified and the default section is None. + """ + # resolve the section + if section is None: + if self.namespace is None: + raise configparser.Error("No section specified. Please specify a section or set StateFile.namespace.") + section = self.namespace + + # create section if it does not exist + if section not in self.loaded_cfg.sections(): + self.loaded_cfg.add_section(section) + # set the variable + self.loaded_cfg.set(section, key, value) + + def get_variable(self, key: str, section: str | None = None) -> Any: + """Get a variable from the configuration object. + + Note: + Since we use the ConfigParser class, the section names are case-sensitive but the keys are not. + + Args: + key: The key of the variable to be loaded. + section: The section of the configuration object to read the variable from. + Defaults to None, in which case the default section is used. + + Returns: + The value of the variable. It is None if the key does not exist. + + Raises: + configparser.Error: If no section is specified and the default section is None. + """ + # resolve the section + if section is None: + if self.namespace is None: + raise configparser.Error("No section specified. Please specify a section or set StateFile.namespace.") + section = self.namespace + + return self.loaded_cfg.get(section, key, fallback=None) + + def delete_variable(self, key: str, section: str | None = None): + """Delete a variable from the configuration object. + + Note: + Since we use the ConfigParser class, the section names are case-sensitive but the keys are not. + + Args: + key: The key of the variable to be deleted. + section: The section of the configuration object to remove the variable from. + Defaults to None, in which case the default section is used. + + Raises: + configparser.Error: If no section is specified and the default section is None. + configparser.NoSectionError: If the section does not exist in the configuration object. + configparser.NoOptionError: If the key does not exist in the section. + """ + # resolve the section + if section is None: + if self.namespace is None: + raise configparser.Error("No section specified. Please specify a section or set StateFile.namespace.") + section = self.namespace + + # check if the section exists + if section not in self.loaded_cfg.sections(): + raise configparser.NoSectionError(f"Section '{section}' does not exist in the file: {self.path}") + + # check if the key exists + if self.loaded_cfg.has_option(section, key): + self.loaded_cfg.remove_option(section, key) + else: + raise configparser.NoOptionError(option=key, section=section) + + """ + Operations - File I/O. + """ + + def load(self): + """Load the configuration file into memory. + + This function reads the contents of the configuration file into memory. + If the file does not exist, it creates an empty file. + """ + self.loaded_cfg = ConfigParser() + self.loaded_cfg.read(self.path) + + def save(self): + """Save the configuration file to disk.""" + with open(self.path, "w+") as f: + self.loaded_cfg.write(f) diff --git a/docker/utils/x11_utils.py b/docker/utils/x11_utils.py new file mode 100644 index 0000000..4001d10 --- /dev/null +++ b/docker/utils/x11_utils.py @@ -0,0 +1,227 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Utility functions for managing X11 forwarding in the docker container.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +from .state_file import StateFile + + +# This method of x11 enabling forwarding was inspired by osrf/rocker +# https://github.com/osrf/rocker +def configure_x11(statefile: StateFile) -> dict[str, str]: + """Configure X11 forwarding by creating and managing a temporary .xauth file. + + If xauth is not installed, the function prints an error message and exits. The message + instructs the user to install xauth with 'apt install xauth'. + + If the .xauth file does not exist, the function creates it and configures it with the necessary + xauth cookie. + + Args: + statefile: An instance of the configuration file class. + + Returns: + A dictionary with two key-value pairs: + + - "__UWLAB_TMP_XAUTH": The path to the temporary .xauth file. + - "__UWLAB_TMP_DIR": The path to the directory where the temporary .xauth file is stored. + + """ + # check if xauth is installed + if not shutil.which("xauth"): + print("[INFO] xauth is not installed.") + print("[INFO] Please install it with 'apt install xauth'") + exit(1) + + # set the namespace to X11 for the statefile + statefile.namespace = "X11" + # load the value of the temporary xauth file + tmp_xauth_value = statefile.get_variable("__UWLAB_TMP_XAUTH") + + if tmp_xauth_value is None or not Path(tmp_xauth_value).exists(): + # create a temporary directory to store the .xauth file + tmp_dir = subprocess.run(["mktemp", "-d"], capture_output=True, text=True, check=True).stdout.strip() + # create the .xauth file + tmp_xauth_value = create_x11_tmpfile(tmpdir=Path(tmp_dir)) + # set the statefile variable + statefile.set_variable("__UWLAB_TMP_XAUTH", str(tmp_xauth_value)) + else: + tmp_dir = Path(tmp_xauth_value).parent + + return {"__UWLAB_TMP_XAUTH": str(tmp_xauth_value), "__UWLAB_TMP_DIR": str(tmp_dir)} + + +def x11_check(statefile: StateFile) -> tuple[list[str], dict[str, str]] | None: + """Check and configure X11 forwarding based on user input and existing state. + + This function checks if X11 forwarding is enabled in the configuration file. If it is not configured, + the function prompts the user to enable or disable X11 forwarding. If X11 forwarding is enabled, the function + configures X11 forwarding by creating a temporary .xauth file. + + Args: + statefile: An instance of the configuration file class. + + Returns: + If X11 forwarding is enabled, the function returns a tuple containing the following: + + - A list containing the x11.yaml file configuration option for docker-compose. + - A dictionary containing the environment variables for the container. + + If X11 forwarding is disabled, the function returns None. + """ + # set the namespace to X11 for the statefile + statefile.namespace = "X11" + # check if X11 forwarding is enabled + is_x11_forwarding_enabled = statefile.get_variable("X11_FORWARDING_ENABLED") + + if is_x11_forwarding_enabled is None: + print("[INFO] X11 forwarding from the UW Lab container is disabled by default.") + print( + "[INFO] It will fail if there is no display, or this script is being run via ssh without proper" + " configuration." + ) + x11_answer = input("Would you like to enable it? (y/N) ") + + # parse the user's input + if x11_answer.lower() == "y": + is_x11_forwarding_enabled = "1" + print("[INFO] X11 forwarding is enabled from the container.") + else: + is_x11_forwarding_enabled = "0" + print("[INFO] X11 forwarding is disabled from the container.") + + # remember the user's choice and set the statefile variable + statefile.set_variable("X11_FORWARDING_ENABLED", is_x11_forwarding_enabled) + else: + # print the current configuration + print(f"[INFO] X11 Forwarding is configured as '{is_x11_forwarding_enabled}' in '.container.cfg'.") + + # print help message to enable/disable X11 forwarding + if is_x11_forwarding_enabled == "1": + print("\tTo disable X11 forwarding, set 'X11_FORWARDING_ENABLED=0' in '.container.cfg'.") + else: + print("\tTo enable X11 forwarding, set 'X11_FORWARDING_ENABLED=1' in '.container.cfg'.") + + if is_x11_forwarding_enabled == "1": + x11_envars = configure_x11(statefile) + # If X11 forwarding is enabled, return the proper args to + # compose the x11.yaml file. Else, return an empty string. + return ["--file", "x11.yaml"], x11_envars + + return None + + +def x11_cleanup(statefile: StateFile): + """Clean up the temporary .xauth file used for X11 forwarding. + + If the .xauth file exists, this function deletes it and remove the corresponding state variable. + + Args: + statefile: An instance of the configuration file class. + """ + # set the namespace to X11 for the statefile + statefile.namespace = "X11" + + # load the value of the temporary xauth file + tmp_xauth_value = statefile.get_variable("__UWLAB_TMP_XAUTH") + + # if the file exists, delete it and remove the state variable + if tmp_xauth_value is not None and Path(tmp_xauth_value).exists(): + print(f"[INFO] Removing temporary UW Lab '.xauth' file: {tmp_xauth_value}.") + Path(tmp_xauth_value).unlink() + statefile.delete_variable("__UWLAB_TMP_XAUTH") + + +def create_x11_tmpfile(tmpfile: Path | None = None, tmpdir: Path | None = None) -> Path: + """Creates an .xauth file with an MIT-MAGIC-COOKIE derived from the current ``DISPLAY`` environment variable. + + Args: + tmpfile: A Path to a file which will be filled with the correct .xauth info. + tmpdir: A Path to the directory where a random tmp file will be made. + This is used as an ``--tmpdir arg`` to ``mktemp`` bash command. + + Returns: + The Path to the .xauth file. + """ + if tmpfile is None: + if tmpdir is None: + add_tmpdir = "" + else: + add_tmpdir = f"--tmpdir={tmpdir}" + # Create .tmp file with .xauth suffix + tmp_xauth = Path( + subprocess.run( + ["mktemp", "--suffix=.xauth", f"{add_tmpdir}"], capture_output=True, text=True, check=True + ).stdout.strip() + ) + else: + tmpfile.touch() + tmp_xauth = tmpfile + + # Derive current MIT-MAGIC-COOKIE and make it universally addressable + xauth_cookie = subprocess.run( + ["xauth", "nlist", os.environ["DISPLAY"]], capture_output=True, text=True, check=True + ).stdout.replace("ffff", "") + + # Merge the new cookie into the create .tmp file + subprocess.run(["xauth", "-f", tmp_xauth, "nmerge", "-"], input=xauth_cookie, text=True, check=True) + + return tmp_xauth + + +def x11_refresh(statefile: StateFile): + """Refresh the temporary .xauth file used for X11 forwarding. + + If x11 is enabled, this function generates a new .xauth file with the current MIT-MAGIC-COOKIE-1. + The new file uses the same filename so that the bind-mount and ``XAUTHORITY`` var from build-time + still work. + + As the envar ``DISPLAY` informs the contents of the MIT-MAGIC-COOKIE-1, that value within the container + will also need to be updated to the current value on the host. Currently, this done automatically in + :meth:`ContainerInterface.enter` method. + + The function exits if X11 forwarding is enabled but the temporary .xauth file does not exist. In this case, + the user must rebuild the container. + + Args: + statefile: An instance of the configuration file class. + """ + # set the namespace to X11 for the statefile + statefile.namespace = "X11" + + # check if X11 forwarding is enabled + is_x11_forwarding_enabled = statefile.get_variable("X11_FORWARDING_ENABLED") + # load the value of the temporary xauth file + tmp_xauth_value = statefile.get_variable("__UWLAB_TMP_XAUTH") + + # print the current configuration + if is_x11_forwarding_enabled is not None: + status = "enabled" if is_x11_forwarding_enabled == "1" else "disabled" + print(f"[INFO] X11 Forwarding is {status} from the settings in '.container.cfg'") + + # if the file exists, delete it and create a new one + if tmp_xauth_value is not None and Path(tmp_xauth_value).exists(): + # remove the file and create a new one + Path(tmp_xauth_value).unlink() + create_x11_tmpfile(tmpfile=Path(tmp_xauth_value)) + # update the statefile with the new path + statefile.set_variable("__UWLAB_TMP_XAUTH", str(tmp_xauth_value)) + elif tmp_xauth_value is None: + if is_x11_forwarding_enabled is not None and is_x11_forwarding_enabled == "1": + print( + "[ERROR] X11 forwarding is enabled but the temporary .xauth file does not exist." + " Please rebuild the container by running: './docker/container.py start'" + ) + sys.exit(1) + else: + print("[INFO] X11 forwarding is disabled. No action taken.") diff --git a/docker/x11.yaml b/docker/x11.yaml new file mode 100644 index 0000000..5bb8018 --- /dev/null +++ b/docker/x11.yaml @@ -0,0 +1,41 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +services: + uw-lab-base: + environment: + - DISPLAY + - TERM + - QT_X11_NO_MITSHM=1 + - XAUTHORITY=${__UWLAB_TMP_XAUTH} + volumes: + - type: bind + source: ${__UWLAB_TMP_DIR} + target: ${__UWLAB_TMP_DIR} + - type: bind + source: /tmp/.X11-unix + target: /tmp/.X11-unix + - type: bind + source: /etc/localtime + target: /etc/localtime + read_only: true + + uw-lab-ros2: + environment: + - DISPLAY + - TERM + - QT_X11_NO_MITSHM=1 + - XAUTHORITY=${__UWLAB_TMP_XAUTH} + volumes: + - type: bind + source: ${__UWLAB_TMP_DIR} + target: ${__UWLAB_TMP_DIR} + - type: bind + source: /tmp/.X11-unix + target: /tmp/.X11-unix + - type: bind + source: /etc/localtime + target: /etc/localtime + read_only: true diff --git a/docs/Makefile b/docs/Makefile index ce33dad..0fe0702 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,4 +15,10 @@ multi-docs: .PHONY: current-docs current-docs: - @$(SPHINXBUILD) "$(SOURCEDIR)" "$(BUILDDIR)/current" $(SPHINXOPTS) + @rm -rf "$(BUILDDIR)/current" + @$(SPHINXBUILD) -W --keep-going "$(SOURCEDIR)" "$(BUILDDIR)/current" $(SPHINXOPTS) + +.PHONY: serve-current +serve-current: current-docs + @echo "Serving docs at http://127.0.0.1:8000 (Ctrl+C to stop)" + @python -m http.server 8000 --bind 127.0.0.1 diff --git a/docs/README.md b/docs/README.md index 69a77a4..e3a86ec 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,9 @@ make current-docs # 3. Open the current docs xdg-open _build/current/index.html + +# 3. Option B: Serve locally (handy over SSH) +make serve-current # serves http://127.0.0.1:8000 ``` diff --git a/docs/_redirect/index.html b/docs/_redirect/index.html index 5208597..50a9106 100644 --- a/docs/_redirect/index.html +++ b/docs/_redirect/index.html @@ -1,7 +1,7 @@ - Redirecting to the latest Isaac Lab documentation + Redirecting to the latest UW Lab documentation diff --git a/docs/conf.py b/docs/conf.py index 8932e47..816f73e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,8 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause + # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full @@ -19,10 +20,16 @@ sys.path.insert(0, os.path.abspath("../source/uwlab")) sys.path.insert(0, os.path.abspath("../source/uwlab/uwlab")) -sys.path.insert(0, os.path.abspath("../source/uwlab_tasks")) -sys.path.insert(0, os.path.abspath("../source/uwlab_tasks/uwlab_tasks")) sys.path.insert(0, os.path.abspath("../source/uwlab_apps")) sys.path.insert(0, os.path.abspath("../source/uwlab_apps/uwlab_apps")) +sys.path.insert(0, os.path.abspath("../source/uwlab_tasks")) +sys.path.insert(0, os.path.abspath("../source/uwlab_tasks/uwlab_tasks")) +sys.path.insert(0, os.path.abspath("../source/uwlab_rl")) +sys.path.insert(0, os.path.abspath("../source/uwlab_rl/uwlab_rl")) +sys.path.insert(0, os.path.abspath("../source/uwlab_mimic")) +sys.path.insert(0, os.path.abspath("../source/uwlab_mimic/uwlab_mimic")) +sys.path.insert(0, os.path.abspath("../source/uwlab_assets")) +sys.path.insert(0, os.path.abspath("../source/uwlab_assets/uwlab_assets")) # -- Project information ----------------------------------------------------- @@ -82,6 +89,17 @@ # TODO: Enable this by default once we have fixed all the warnings # nitpicky = True +nitpick_ignore = [ + ("py:obj", "slice(None)"), +] + +nitpick_ignore_regex = [ + (r"py:.*", r"pxr.*"), # we don't have intersphinx mapping for pxr + (r"py:.*", r"trimesh.*"), # we don't have intersphinx mapping for trimesh +] + +# emoji style +sphinxemoji_style = "twemoji" # options: "twemoji" or "unicode" # put type hints inside the signature instead of the description (easier to maintain) autodoc_typehints = "signature" # autodoc_typehints_format = "fully-qualified" @@ -107,10 +125,12 @@ intersphinx_mapping = { "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), - "isaac": ("https://docs.omniverse.nvidia.com/py/isaacsim", 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), } # Add any paths that contain templates here, relative to this directory. @@ -124,6 +144,7 @@ # Mock out modules that are not available on RTD autodoc_mock_imports = [ "torch", + "torchvision", "numpy", "matplotlib", "scipy", @@ -132,9 +153,6 @@ "pxr", "isaacsim", "omni", - "pyrealsense2", - "cv2", - "lz4", "omni.kit", "omni.log", "omni.usd", @@ -144,17 +162,11 @@ "pxr.PhysxSchema", "pxr.PhysicsSchemaTools", "omni.replicator", - "omni.isaac.core", - "omni.isaac.kit", - "omni.isaac.cloner", - "omni.isaac.urdf", - "omni.isaac.version", - "omni.isaac.motion_generation", - "omni.isaac.ui", "isaacsim", "isaacsim.core.api", "isaacsim.core.cloner", "isaacsim.core.version", + "isaacsim.core.utils", "isaacsim.robot_motion.motion_generation", "isaacsim.gui.components", "isaacsim.asset.importer.urdf", @@ -175,10 +187,13 @@ "tensordict", "trimesh", "toml", - "isaaclab", - "isaaclab_assets", - "isaaclab_tasks", - "isaaclab_rl", + "pink", + "pinocchio", + "nvidia.srl", + "flatdict", + "IPython", + "ipywidgets", + "mpl_toolkits", ] # List of zero or more Sphinx-specific warning categories to be squelched (i.e., @@ -226,8 +241,9 @@ html_css_files = ["custom.css"] html_theme_options = { + "path_to_docs": "docs/", "collapse_navigation": True, - "repository_url": "https://github.com/UW-Lab/UWLab", + "repository_url": "https://github.com/uw-lab/UWLab", "use_repository_button": True, "use_issues_button": True, "use_edit_page_button": True, @@ -235,26 +251,26 @@ "use_sidenotes": True, "logo": { "text": "UW Lab Documentation", - "image_light": "source/_static/UW-logo-black.png", - "image_dark": "source/_static/UW-logo-white.png", + "image_light": "source/_static/UW-logo-white.png", + "image_dark": "source/_static/UW-logo-black.png", }, "icon_links": [ { - "name": "UWLab", - "url": "https://github.com/UW-Lab/UWLab", - "icon": "fa-brands fa-github-alt", - "type": "fontawesome", - }, - { - "name": "IsaacLab", - "url": "https://github.com/isaac-sim/IsaacLab", + "name": "GitHub", + "url": "https://github.com/uw-lab/UWLab", "icon": "fa-brands fa-square-github", "type": "fontawesome", }, { "name": "Isaac Sim", "url": "https://developer.nvidia.com/isaac-sim", - "icon": "https://img.shields.io/badge/IsaacSim-4.5.0-silver.svg", + "icon": "https://img.shields.io/badge/IsaacSim-5.1.0-silver.svg", + "type": "url", + }, + { + "name": "Stars", + "url": "https://img.shields.io/github/stars/uw-lab/UWLab?color=fedcba", + "icon": "https://img.shields.io/github/stars/uw-lab/UWLab?color=fedcba", "type": "url", }, ], @@ -268,7 +284,7 @@ # Whitelist pattern for remotes smv_remote_whitelist = r"^.*$" # Whitelist pattern for branches (set to None to ignore all branches) -smv_branch_whitelist = os.getenv("SMV_BRANCH_WHITELIST", r"^(main|devel)$") +smv_branch_whitelist = os.getenv("SMV_BRANCH_WHITELIST", r"^(main|devel|release/.*)$") # Whitelist pattern for tags (set to None to ignore all tags) smv_tag_whitelist = os.getenv("SMV_TAG_WHITELIST", r"^v[1-9]\d*\.\d+\.\d+$") html_sidebars = { diff --git a/docs/index.rst b/docs/index.rst index 35713d2..222a397 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,12 +1,16 @@ -Overview -======== +Welcome to UW Lab! +===================== -.. figure:: source/_static/cover.png +.. figure:: source/_static/uwlab.jpg :width: 100% - :alt: Cover + :alt: UW LAB Robots + +.. note:: + *We gratefully acknowledge that this documentation builds upon the NVIDIA Isaac Lab documentation with minimal namespace updates* + Preamble -================== +======== **UW Lab** build upon the solid foundation laid by ``Isaac Lab`` / ``NVIDIA Isaac Sim``, expanding its framework to embrace a broader spectrum of algorithms, robots, and environments. While adhering to the @@ -23,7 +27,7 @@ committed to crafting a value where the process is as significant as the outcome deeply with our vision. -License +LICENSE ======= The UW Lab framework is open-sourced under the BSD-3-Clause license. @@ -32,21 +36,8 @@ Please refer to :ref:`license` for more details. Acknowledgement =============== - -If you found UW Lab useful, we appreciate if you cite it in academic publications: - -.. code:: bibtex - - @software{zhang2025uwlab, - author={Zhang, Zhengyu and Yu, Feng and Castro, Mateo and Yin, Patrick and Peng, Quanquan and Scalise, Rosario}, - title={{UWLab}: A Simulation Platform for Robot Learning Environment}, - year={2025}, - url={https://github.com/UW-Lab/UWLab} - } - - UW Lab development initiated from the `Orbit `_ framework. -please cite orbit in academic publications as well in honor of the original authors.: +We would appreciate if you would cite it in academic publications as well: .. code:: bibtex @@ -69,7 +60,15 @@ Table of Contents :maxdepth: 2 :caption: Getting Started - source/setup/installation/local_installation + source/setup/installation/index + source/deployment/index + +.. toctree:: + :maxdepth: 2 + :caption: Getting Started + :titlesonly: + + source/overview/developer-guide/index .. toctree:: :maxdepth: 1 @@ -77,6 +76,7 @@ Table of Contents :titlesonly: source/publications/pg1 + source/publications/omnireset/index .. toctree:: :maxdepth: 3 @@ -108,4 +108,4 @@ Indices and tables * :ref:`modindex` * :ref:`search` -.. _NVIDIA Isaac Sim: https://docs.omniverse.nvidia.com/isaacsim/latest/index.html +.. _NVIDIA Isaac Sim: https://docs.isaacsim.omniverse.nvidia.com/latest/index.html diff --git a/docs/licenses/assets/valkyrie-license b/docs/licenses/assets/valkyrie-license new file mode 100644 index 0000000..8421ac1 --- /dev/null +++ b/docs/licenses/assets/valkyrie-license @@ -0,0 +1,249 @@ +NASA OPEN SOURCE AGREEMENT VERSION 1.3 + +THIS OPEN SOURCE AGREEMENT ("AGREEMENT") DEFINES THE RIGHTS OF USE, +REPRODUCTION, DISTRIBUTION, MODIFICATION AND REDISTRIBUTION OF CERTAIN +COMPUTER SOFTWARE ORIGINALLY RELEASED BY THE UNITED STATES GOVERNMENT +AS REPRESENTED BY THE GOVERNMENT AGENCY LISTED BELOW ("GOVERNMENT +AGENCY"). THE UNITED STATES GOVERNMENT, AS REPRESENTED BY GOVERNMENT +AGENCY, IS AN INTENDED THIRD-PARTY BENEFICIARY OF ALL SUBSEQUENT +DISTRIBUTIONS OR REDISTRIBUTIONS OF THE SUBJECT SOFTWARE. ANYONE WHO +USES, REPRODUCES, DISTRIBUTES, MODIFIES OR REDISTRIBUTES THE SUBJECT +SOFTWARE, AS DEFINED HEREIN, OR ANY PART THEREOF, IS, BY THAT ACTION, +ACCEPTING IN FULL THE RESPONSIBILITIES AND OBLIGATIONS CONTAINED IN +THIS AGREEMENT. + +Government Agency: National Aeronautics and Space Administration (NASA) +Government Agency Original Software Designation: GSC-16256-1 +Government Agency Original Software Title: "ISTP CDF Skeleton Editor" +User Registration Requested. Please Visit https://spdf.gsfc.nasa.gov/ +Government Agency Point of Contact for Original Software: + NASA-SPDF-Support@nasa.onmicrosoft.com + + +1. DEFINITIONS + +A. "Contributor" means Government Agency, as the developer of the +Original Software, and any entity that makes a Modification. +B. "Covered Patents" mean patent claims licensable by a Contributor +that are necessarily infringed by the use or sale of its Modification +alone or when combined with the Subject Software. +C. "Display" means the showing of a copy of the Subject Software, +either directly or by means of an image, or any other device. +D. "Distribution" means conveyance or transfer of the Subject +Software, regardless of means, to another. +E. "Larger Work" means computer software that combines Subject +Software, or portions thereof, with software separate from the Subject +Software that is not governed by the terms of this Agreement. +F. "Modification" means any alteration of, including addition to or +deletion from, the substance or structure of either the Original +Software or Subject Software, and includes derivative works, as that +term is defined in the Copyright Statute, 17 USC 101. However, the +act of including Subject Software as part of a Larger Work does not in +and of itself constitute a Modification. +G. "Original Software" means the computer software first released +under this Agreement by Government Agency with Government Agency +designation "GSC-16256-1"" and entitled +"ISTP CDF Skeleton Editor", including source code, +object code and accompanying documentation, if any. +H. "Recipient" means anyone who acquires the Subject Software under +this Agreement, including all Contributors. +I. "Redistribution" means Distribution of the Subject Software after a +Modification has been made. +J. "Reproduction" means the making of a counterpart, image or copy of +the Subject Software. +K. "Sale" means the exchange of the Subject Software for money or +equivalent value. +L. "Subject Software" means the Original Software, Modifications, or +any respective parts thereof. +M. "Use" means the application or employment of the Subject Software +for any purpose. + +2. GRANT OF RIGHTS + +A. Under Non-Patent Rights: Subject to the terms and conditions of +this Agreement, each Contributor, with respect to its own contribution +to the Subject Software, hereby grants to each Recipient a +non-exclusive, world-wide, royalty-free license to engage in the +following activities pertaining to the Subject Software: + +1. Use +2. Distribution +3. Reproduction +4. Modification +5. Redistribution +6. Display + +B. Under Patent Rights: Subject to the terms and conditions of this +Agreement, each Contributor, with respect to its own contribution to +the Subject Software, hereby grants to each Recipient under Covered +Patents a non-exclusive, world-wide, royalty-free license to engage in +the following activities pertaining to the Subject Software: + +1. Use +2. Distribution +3. Reproduction +4. Sale +5. Offer for Sale + +C. The rights granted under Paragraph B. also apply to the combination +of a Contributor's Modification and the Subject Software if, at the +time the Modification is added by the Contributor, the addition of +such Modification causes the combination to be covered by the Covered +Patents. It does not apply to any other combinations that include a +Modification. + +D. The rights granted in Paragraphs A. and B. allow the Recipient to +sublicense those same rights. Such sublicense must be under the same +terms and conditions of this Agreement. + +3. OBLIGATIONS OF RECIPIENT + +A. Distribution or Redistribution of the Subject Software must be made +under this Agreement except for additions covered under paragraph 3H. + +1. Whenever a Recipient distributes or redistributes the Subject + Software, a copy of this Agreement must be included with each copy + of the Subject Software; and +2. If Recipient distributes or redistributes the Subject Software in + any form other than source code, Recipient must also make the + source code freely available, and must provide with each copy of + the Subject Software information on how to obtain the source code + in a reasonable manner on or through a medium customarily used for + software exchange. + +B. Each Recipient must ensure that the following copyright notice +appears prominently in the Subject Software: + +Copyright (c) 2006 United States Government as represented by the +National Aeronautics and Space Administration. No copyright is claimed +in the United States under Title 17, U.S.Code. All Other Rights Reserved. + +C. Each Contributor must characterize its alteration of the Subject +Software as a Modification and must identify itself as the originator +of its Modification in a manner that reasonably allows subsequent +Recipients to identify the originator of the Modification. In +fulfillment of these requirements, Contributor must include a file +(e.g., a change log file) that describes the alterations made and the +date of the alterations, identifies Contributor as originator of the +alterations, and consents to characterization of the alterations as a +Modification, for example, by including a statement that the +Modification is derived, directly or indirectly, from Original +Software provided by Government Agency. Once consent is granted, it +may not thereafter be revoked. + +D. A Contributor may add its own copyright notice to the Subject +Software. Once a copyright notice has been added to the Subject +Software, a Recipient may not remove it without the express permission +of the Contributor who added the notice. + +E. A Recipient may not make any representation in the Subject Software +or in any promotional, advertising or other material that may be +construed as an endorsement by Government Agency or by any prior +Recipient of any product or service provided by Recipient, or that may +seek to obtain commercial advantage by the fact of Government Agency's +or a prior Recipient's participation in this Agreement. + +F. In an effort to track usage and maintain accurate records of the +Subject Software, each Recipient, upon receipt of the Subject +Software, is requested to register with Government Agency by visiting +the following website: https://opensource.gsfc.nasa.gov/. Recipient's +name and personal information shall be used for statistical purposes +only. Once a Recipient makes a Modification available, it is requested +that the Recipient inform Government Agency at the web site provided +above how to access the Modification. + +G. Each Contributor represents that that its Modification is believed +to be Contributor's original creation and does not violate any +existing agreements, regulations, statutes or rules, and further that +Contributor has sufficient rights to grant the rights conveyed by this +Agreement. + +H. A Recipient may choose to offer, and to charge a fee for, warranty, +support, indemnity and/or liability obligations to one or more other +Recipients of the Subject Software. A Recipient may do so, however, +only on its own behalf and not on behalf of Government Agency or any +other Recipient. Such a Recipient must make it absolutely clear that +any such warranty, support, indemnity and/or liability obligation is +offered by that Recipient alone. Further, such Recipient agrees to +indemnify Government Agency and every other Recipient for any +liability incurred by them as a result of warranty, support, indemnity +and/or liability offered by such Recipient. + +I. A Recipient may create a Larger Work by combining Subject Software +with separate software not governed by the terms of this agreement and +distribute the Larger Work as a single product. In such case, the +Recipient must make sure Subject Software, or portions thereof, +included in the Larger Work is subject to this Agreement. + +J. Notwithstanding any provisions contained herein, Recipient is +hereby put on notice that export of any goods or technical data from +the United States may require some form of export license from the +U.S. Government. Failure to obtain necessary export licenses may +result in criminal liability under U.S. laws. Government Agency +neither represents that a license shall not be required nor that, if +required, it shall be issued. Nothing granted herein provides any +such export license. + +4. DISCLAIMER OF WARRANTIES AND LIABILITIES; WAIVER AND INDEMNIFICATION + +A. No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY +WARRANTY OF ANY KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, +INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SUBJECT SOFTWARE +WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM +INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL BE ERROR +FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO +THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, +CONSTITUTE AN ENDORSEMENT BY GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT +OF ANY RESULTS, RESULTING DESIGNS, HARDWARE, SOFTWARE PRODUCTS OR ANY +OTHER APPLICATIONS RESULTING FROM USE OF THE SUBJECT SOFTWARE. +FURTHER, GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND LIABILITIES +REGARDING THIRD-PARTY SOFTWARE, IF PRESENT IN THE ORIGINAL SOFTWARE, +AND DISTRIBUTES IT "AS IS." + +B. Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS +AGAINST THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND +SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF RECIPIENT'S USE OF +THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES, +EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM +PRODUCTS BASED ON, OR RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT +SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD HARMLESS THE UNITED +STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY +PRIOR RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE +REMEDY FOR ANY SUCH MATTER SHALL BE THE IMMEDIATE, UNILATERAL +TERMINATION OF THIS AGREEMENT. + + +5. GENERAL TERMS + +A. Termination: This Agreement and the rights granted hereunder will +terminate automatically if a Recipient fails to comply with these +terms and conditions, and fails to cure such noncompliance within +thirty (30) days of becoming aware of such noncompliance. Upon +termination, a Recipient agrees to immediately cease use and +distribution of the Subject Software. All sublicenses to the Subject +Software properly granted by the breaching Recipient shall survive any +such termination of this Agreement. + +B. Severability: If any provision of this Agreement is invalid or +unenforceable under applicable law, it shall not affect the validity +or enforceability of the remainder of the terms of this Agreement. + +C. Applicable Law: This Agreement shall be subject to United States +federal law only for all purposes, including, but not limited to, +determining the validity of this Agreement, the meaning of its +provisions and the rights, obligations and remedies of the parties. + +D. Entire Understanding: This Agreement constitutes the entire +understanding and agreement of the parties relating to release of the +Subject Software and may not be superseded, modified or amended except +by further written agreement duly executed by the parties. + +E. Binding Authority: By accepting and using the Subject Software +under this Agreement, a Recipient affirms its authority to bind the +Recipient to all terms and conditions of this Agreement and that that +Recipient hereby agrees to all terms and conditions herein. + +F. Point of Contact: Any Recipient contact with Government Agency is +to be directed to the designated representative as follows: +NASA-SPDF-Support@nasa.onmicrosoft.com. diff --git a/docs/licenses/dependencies/Farama-Notifications-license.txt b/docs/licenses/dependencies/Farama-Notifications-license.txt new file mode 100644 index 0000000..44a6bbb --- /dev/null +++ b/docs/licenses/dependencies/Farama-Notifications-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Farama Foundation + +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/GitPython-license.txt b/docs/licenses/dependencies/GitPython-license.txt new file mode 100644 index 0000000..ba8a219 --- /dev/null +++ b/docs/licenses/dependencies/GitPython-license.txt @@ -0,0 +1,29 @@ +Copyright (C) 2008, 2009 Michael Trier and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of the GitPython project nor the names of +its contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/InquirerPy-license.txt b/docs/licenses/dependencies/InquirerPy-license.txt new file mode 100644 index 0000000..7f22fe2 --- /dev/null +++ b/docs/licenses/dependencies/InquirerPy-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Kevin Zhuang + +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/absl-py-license.txt b/docs/licenses/dependencies/absl-py-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/absl-py-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/accessible-pygments-license.txt b/docs/licenses/dependencies/accessible-pygments-license.txt new file mode 100644 index 0000000..6779ad4 --- /dev/null +++ b/docs/licenses/dependencies/accessible-pygments-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Quansight Labs +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/alabaster-license.txt b/docs/licenses/dependencies/alabaster-license.txt new file mode 100644 index 0000000..8f1c197 --- /dev/null +++ b/docs/licenses/dependencies/alabaster-license.txt @@ -0,0 +1,12 @@ +Copyright (c) 2020 Jeff Forcier. + +Based on original work copyright (c) 2011 Kenneth Reitz and copyright (c) 2010 Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/antlr4_python3_runtime-license.txt b/docs/licenses/dependencies/antlr4_python3_runtime-license.txt new file mode 100644 index 0000000..5d27694 --- /dev/null +++ b/docs/licenses/dependencies/antlr4_python3_runtime-license.txt @@ -0,0 +1,28 @@ +Copyright (c) 2012-2022 The ANTLR Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither name of copyright holders nor the names of its contributors +may be used to endorse or promote products derived from this software +without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/anyio-license.txt b/docs/licenses/dependencies/anyio-license.txt new file mode 100644 index 0000000..104eebf --- /dev/null +++ b/docs/licenses/dependencies/anyio-license.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 Alex GrΓΆnholm + +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/anytree-license.txt b/docs/licenses/dependencies/anytree-license.txt new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/docs/licenses/dependencies/anytree-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/assimp-license.txt b/docs/licenses/dependencies/assimp-license.txt new file mode 100644 index 0000000..c66fa94 --- /dev/null +++ b/docs/licenses/dependencies/assimp-license.txt @@ -0,0 +1,78 @@ +Open Asset Import Library (assimp) + +Copyright (c) 2006-2021, assimp team +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the assimp team, nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of the assimp team. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +****************************************************************************** + +AN EXCEPTION applies to all files in the ./test/models-nonbsd folder. +These are 3d models for testing purposes, from various free sources +on the internet. They are - unless otherwise stated - copyright of +their respective creators, which may impose additional requirements +on the use of their work. For any of these models, see +.source.txt for more legal information. Contact us if you +are a copyright holder and believe that we credited you improperly or +if you don't want your files to appear in the repository. + + +****************************************************************************** + +Poly2Tri Copyright (c) 2009-2010, Poly2Tri Contributors +http://code.google.com/p/poly2tri/ + +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Poly2Tri nor the names of its contributors may be + used to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/asttokens-license.txt b/docs/licenses/dependencies/asttokens-license.txt new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/docs/licenses/dependencies/asttokens-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/attrs-license b/docs/licenses/dependencies/attrs-license new file mode 100644 index 0000000..2bd6453 --- /dev/null +++ b/docs/licenses/dependencies/attrs-license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack and the attrs contributors + +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/autodocsumm-license.txt b/docs/licenses/dependencies/autodocsumm-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/autodocsumm-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/babel-license.txt b/docs/licenses/dependencies/babel-license.txt new file mode 100644 index 0000000..f31575e --- /dev/null +++ b/docs/licenses/dependencies/babel-license.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +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/beautifulsoup4-license.txt b/docs/licenses/dependencies/beautifulsoup4-license.txt new file mode 100644 index 0000000..d668d13 --- /dev/null +++ b/docs/licenses/dependencies/beautifulsoup4-license.txt @@ -0,0 +1,26 @@ +Beautiful Soup is made available under the MIT license: + + Copyright (c) 2004-2012 Leonard Richardson + + 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, DAMMIT. + +Beautiful Soup incorporates code from the html5lib library, which is +also made available under the MIT license. diff --git a/docs/licenses/dependencies/boost-license.txt b/docs/licenses/dependencies/boost-license.txt new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/docs/licenses/dependencies/boost-license.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN 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/botocore-license.txt b/docs/licenses/dependencies/botocore-license.txt new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/docs/licenses/dependencies/botocore-license.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/docs/licenses/dependencies/certifi-license.txt b/docs/licenses/dependencies/certifi-license.txt new file mode 100644 index 0000000..62b076c --- /dev/null +++ b/docs/licenses/dependencies/certifi-license.txt @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/docs/licenses/dependencies/charset-normalizer-license.txt b/docs/licenses/dependencies/charset-normalizer-license.txt new file mode 100644 index 0000000..9725772 --- /dev/null +++ b/docs/licenses/dependencies/charset-normalizer-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 TAHRI Ahmed R. + +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/click-license.txt b/docs/licenses/dependencies/click-license.txt new file mode 100644 index 0000000..d12a849 --- /dev/null +++ b/docs/licenses/dependencies/click-license.txt @@ -0,0 +1,28 @@ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/cloudpickle-license.txt b/docs/licenses/dependencies/cloudpickle-license.txt new file mode 100644 index 0000000..d112c48 --- /dev/null +++ b/docs/licenses/dependencies/cloudpickle-license.txt @@ -0,0 +1,32 @@ +This module was extracted from the `cloud` package, developed by +PiCloud, Inc. + +Copyright (c) 2015, Cloudpickle contributors. +Copyright (c) 2012, Regents of the University of California. +Copyright (c) 2009 PiCloud, Inc. http://www.picloud.com. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the University of California, Berkeley nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/cmeel-license.txt b/docs/licenses/dependencies/cmeel-license.txt new file mode 100644 index 0000000..664756e --- /dev/null +++ b/docs/licenses/dependencies/cmeel-license.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2022, Guilhem Saurel +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/colorama-license.txt b/docs/licenses/dependencies/colorama-license.txt new file mode 100644 index 0000000..3105888 --- /dev/null +++ b/docs/licenses/dependencies/colorama-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2010 Jonathan Hartley +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holders, nor those of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/comm-license.txt b/docs/licenses/dependencies/comm-license.txt new file mode 100644 index 0000000..eee1b58 --- /dev/null +++ b/docs/licenses/dependencies/comm-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Jupyter +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/console-bridge-license.txt b/docs/licenses/dependencies/console-bridge-license.txt new file mode 100644 index 0000000..574ef07 --- /dev/null +++ b/docs/licenses/dependencies/console-bridge-license.txt @@ -0,0 +1,25 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/contourpy-license.txt b/docs/licenses/dependencies/contourpy-license.txt new file mode 100644 index 0000000..93e41fb --- /dev/null +++ b/docs/licenses/dependencies/contourpy-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021-2025, ContourPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/cuRobo-license.txt b/docs/licenses/dependencies/cuRobo-license.txt new file mode 100644 index 0000000..2b76a56 --- /dev/null +++ b/docs/licenses/dependencies/cuRobo-license.txt @@ -0,0 +1,93 @@ +NVIDIA ISAAC LAB ADDITIONAL SOFTWARE AND MATERIALS LICENSE + +IMPORTANT NOTICE – PLEASE READ AND AGREE BEFORE USING THE SOFTWARE + +This software license agreement ("Agreement") is a legal agreement between you, whether an individual or entity, ("you") and NVIDIA Corporation ("NVIDIA") and governs the use of the NVIDIA cuRobo and related software and materials that NVIDIA delivers to you under this Agreement ("Software"). NVIDIA and you are each a "party" and collectively the "parties." + +By using the Software, you are affirming that you have read and agree to this Agreement. + +If you don't accept all the terms and conditions below, do not use the Software. + +1. License Grant. The Software made available by NVIDIA to you is licensed, not sold. Subject to the terms of this Agreement, NVIDIA grants you a limited, non-exclusive, revocable, non-transferable, and non-sublicensable (except as expressly granted in this Agreement), license to install and use copies of the Software together with NVIDIA Isaac Lab in systems with NVIDIA GPUs ("Purpose"). + +2. License Restrictions. Your license to use the Software is restricted as stated in this Section 2 ("License Restrictions"). You will cooperate with NVIDIA and, upon NVIDIA's written request, you will confirm in writing and provide reasonably requested information to verify your compliance with the terms of this Agreement. You may not: + +2.1 Use the Software for any purpose other than the Purpose, and for clarity use of NVIDIA cuRobo apart from use with Isaac Lab is outside of the Purpose; + +2.2 Sell, rent, sublicense, transfer, distribute or otherwise make available to others (except authorized users as stated in Section 3 ("Authorized Users")) any portion of the Software, except as expressly granted in Section 1 ("License Grant"); + +2.3 Reverse engineer, decompile, or disassemble the Software components provided in binary form, nor attempt in any other manner to obtain source code of such Software; + +2.4 Modify or create derivative works of the Software; + +2.5 Change or remove copyright or other proprietary notices in the Software; + +2.6 Bypass, disable, or circumvent any technical limitation, encryption, security, digital rights management or authentication mechanism in the Software; + +2.7 Use the Software in any manner that would cause them to become subject to an open source software license, subject to the terms in Section 7 ("Components Under Other Licenses"); or + +2.8 Use the Software in violation of any applicable law or regulation in relevant jurisdictions. + +3. Authorized Users. You may allow employees and contractors of your entity or of your subsidiary(ies), and for educational institutions also enrolled students, to internally access and use the Software as authorized by this Agreement from your secure network to perform the work authorized by this Agreement on your behalf. You are responsible for the compliance with the terms of this Agreement by your authorized users. Any act or omission that if committed by you would constitute a breach of this Agreement will be deemed to constitute a breach of this Agreement if committed by your authorized users. + +4. Pre-Release. Software versions identified as alpha, beta, preview, early access or otherwise as pre-release ("Pre-Release") may not be fully functional, may contain errors or design flaws, and may have reduced or different security, privacy, availability and reliability standards relative to NVIDIA commercial offerings. You use Pre-Release Software at your own risk. NVIDIA did not design or test the Software for use in production or business-critical systems. NVIDIA may choose not to make available a commercial version of Pre-Release Software. NVIDIA may also choose to abandon development and terminate the availability of Pre-Release Software at any time without liability. + +5. Updates. NVIDIA may at any time and at its option, change, discontinue, or deprecate any part, or all, of the Software, or change or remove features or functionality, or make available patches, workarounds or other updates to the Software. Unless the updates are provided with their separate governing terms, they are deemed part of the Software licensed to you under this Agreement, and your continued use of the Software is deemed acceptance of such changes. + +6. Components Under Other Licenses. The Software may include or be distributed with components provided with separate legal notices or terms that accompany the components, such as open source software licenses and other license terms ("Other Licenses"). The components are subject to the applicable Other Licenses, including any proprietary notices, disclaimers, requirements and extended use rights; except that this Agreement will prevail regarding the use of third-party open source software, unless a third-party open source software license requires its license terms to prevail. Open source software license means any software, data or documentation subject to any license identified as an open source license by the Open Source Initiative (http://opensource.org), Free Software Foundation (http://www.fsf.org) or other similar open source organization or listed by the Software Package Data Exchange (SPDX) Workgroup under the Linux Foundation (http://www.spdx.org). + +7. Ownership. The Software, including all intellectual property rights, is and will remain the sole and exclusive property of NVIDIA or its licensors. Except as expressly granted in this Agreement, (a) NVIDIA reserves all rights, interests and remedies in connection with the Software, and (b) no other license or right is granted to you by implication, estoppel or otherwise. + +8. Feedback. You may, but you are not obligated to, provide suggestions, requests, fixes, modifications, enhancements, or other feedback regarding the Software (collectively, "Feedback"). Feedback, even if designated as confidential by you, will not create any confidentiality obligation for NVIDIA or its affiliates. If you provide Feedback, you grant NVIDIA, its affiliates and its designees a non-exclusive, perpetual, irrevocable, sublicensable, worldwide, royalty-free, fully paid-up and transferable license, under your intellectual property rights, to publicly perform, publicly display, reproduce, use, make, have made, sell, offer for sale, distribute (through multiple tiers of distribution), import, create derivative works of and otherwise commercialize and exploit the Feedback at NVIDIA's discretion. + +9. Term and Termination. + +9.1 Term and Termination for Convenience. This license ends by July 31, 2026 or earlier at your choice if you finished using the Software for the Purpose. Either party may terminate this Agreement at any time with thirty (30) days' advance written notice to the other party. + +9.2 Termination for Cause. If you commence or participate in any legal proceeding against NVIDIA with respect to the Software, this Agreement will terminate immediately without notice. Either party may terminate this Agreement for cause if: + +(a) The other party fails to cure a material breach of this Agreement within ten (10) days of the non-breaching party's written notice of the breach; or + +(b) the other party breaches its confidentiality obligations or license rights under this Agreement, which termination will be effective immediately upon written notice. + +9.3 Effect of Termination. Upon any expiration or termination of this Agreement, you will promptly stop using and return, delete or destroy NVIDIA confidential information and all Software received under this Agreement. Upon written request, you will certify in writing that you have complied with your obligations under this Section 9.3 ("Effect of Termination"). + +9.4 Survival. Section 5 ("Updates"), Section 6 ("Components Under Other Licenses"), Section 7 ("Ownership"), Section 8 ("Feedback"), Section 9.3 ("Effect of Termination"), Section 9.4 ("Survival"), Section 10 ("Disclaimer of Warranties"), Section 11 ("Limitation of Liability"), Section 12 ("Use in Mission Critical Applications"), Section 13 ("Governing Law and Jurisdiction") and Section 14 ("General") will survive any expiration or termination of this Agreement. + +10. Disclaimer of Warranties. THE SOFTWARE IS PROVIDED BY NVIDIA AS-IS AND WITH ALL FAULTS. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NVIDIA DISCLAIMS ALL WARRANTIES AND REPRESENTATIONS OF ANY KIND, WHETHER EXPRESS, IMPLIED OR STATUTORY, RELATING TO OR ARISING UNDER THIS AGREEMENT, INCLUDING, WITHOUT LIMITATION, THE WARRANTIES OF TITLE, NONINFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, USAGE OF TRADE AND COURSE OF DEALING. NVIDIA DOES NOT WARRANT OR ASSUME RESPONSIBILITY FOR THE ACCURACY OR COMPLETENESS OF ANY THIRD-PARTY INFORMATION, TEXT, GRAPHICS, LINKS CONTAINED IN THE SOFTWARE. WITHOUT LIMITING THE FOREGOING, NVIDIA DOES NOT WARRANT THAT THE SOFTWARE WILL MEET YOUR REQUIREMENTS, ANY DEFECTS OR ERRORS WILL BE CORRECTED, ANY CERTAIN CONTENT WILL BE AVAILABLE; OR THAT THE SOFTWARE IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. NO INFORMATION OR ADVICE GIVEN BY NVIDIA WILL IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY EXPRESSLY PROVIDED IN THIS AGREEMENT. + +11. Limitations of Liability. + +11.1 EXCLUSIONS. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL NVIDIA BE LIABLE FOR ANY (I) INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, OR (II) DAMAGES FOR (A) THE COST OF PROCURING SUBSTITUTE GOODS, OR (B) LOSS OF PROFITS, REVENUES, USE, DATA OR GOODWILL ARISING OUT OF OR RELATED TO THIS AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, OR OTHERWISE, AND EVEN IF NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND EVEN IF A PARTY'S REMEDIES FAIL THEIR ESSENTIAL PURPOSE. + +11.2 DAMAGES CAP. ADDITIONALLY, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NVIDIA'S TOTAL CUMULATIVE AGGREGATE LIABILITY FOR ANY AND ALL LIABILITIES, OBLIGATIONS OR CLAIMS ARISING OUT OF OR RELATED TO THIS AGREEMENT WILL NOT EXCEED FIVE U.S. DOLLARS (US$5). + +12. Use in Mission Critical Applications. You acknowledge that the Software provided under this Agreement is not designed or tested by NVIDIA for use in any system or application where the use or failure of such system or application developed with NVIDIA's Software could result in injury, death or catastrophic damage (each, a "Mission Critical Application"). Examples of Mission Critical Applications include use in avionics, navigation, autonomous vehicle applications, AI solutions for automotive products, military, medical, life support or other mission-critical or life-critical applications. NVIDIA will not be liable to you or any third party, in whole or in part, for any claims or damages arising from these uses. You are solely responsible for ensuring that systems and applications developed with the Software include sufficient safety and redundancy features and comply with all applicable legal and regulatory standards and requirements. + +13. Governing Law and Jurisdiction. This Agreement will be governed in all respects by the laws of the United States and the laws of the State of Delaware, without regard to conflict of laws principles or the United Nations Convention on Contracts for the International Sale of Goods. The state and federal courts residing in Santa Clara County, California will have exclusive jurisdiction over any dispute or claim arising out of or related to this Agreement, and the parties irrevocably consent to personal jurisdiction and venue in those courts; except that either party may apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. + +14. General. + +14.1 Indemnity. By using the Software you agree to defend, indemnify and hold harmless NVIDIA and its affiliates and their respective officers, directors, employees and agents from and against any claims, disputes, demands, liabilities, damages, losses, costs and expenses arising out of or in any way connected with (i) products or services that have been developed or deployed with or use the Software, or claims that they violate laws, or infringe, violate, or misappropriate any third party right; or (ii) your use of the Software in breach of the terms of this Agreement. + +14.2 Independent Contractors. The parties are independent contractors, and this Agreement does not create a joint venture, partnership, agency, or other form of business association between the parties. Neither party will have the power to bind the other party or incur any obligation on its behalf without the other party's prior written consent. Nothing in this Agreement prevents either party from participating in similar arrangements with third parties. + +14.3 No Assignment. NVIDIA may assign, delegate or transfer its rights or obligations under this Agreement by any means or operation of law. You may not, without NVIDIA's prior written consent, assign, delegate or transfer any of your rights or obligations under this Agreement by any means or operation of law, and any attempt to do so is null and void. + +14.4 No Waiver. No failure or delay by a party to enforce any term or obligation of this Agreement will operate as a waiver by that party, or prevent the enforcement of such term or obligation later. + +14.5 Trade Compliance. You agree to comply with all applicable export, import, trade and economic sanctions laws and regulations, as amended, including without limitation U.S. Export Administration Regulations and Office of Foreign Assets Control regulations. You confirm (a) your understanding that export or reexport of certain NVIDIA products or technologies may require a license or other approval from appropriate authorities and (b) that you will not export or reexport any products or technology, directly or indirectly, without first obtaining any required license or other approval from appropriate authorities, (i) to any countries that are subject to any U.S. or local export restrictions (currently including, but not necessarily limited to, Belarus, Cuba, Iran, North Korea, Russia, Syria, the Region of Crimea, Donetsk People's Republic Region and Luhansk People's Republic Region); (ii) to any end-user who you know or have reason to know will utilize them in the design, development or production of nuclear, chemical or biological weapons, missiles, rocket systems, unmanned air vehicles capable of a maximum range of at least 300 kilometers, regardless of payload, or intended for military end-use, or any weapons of mass destruction; (iii) to any end-user who has been prohibited from participating in the U.S. or local export transactions by any governing authority; or (iv) to any known military or military-intelligence end-user or for any known military or military-intelligence end-use in accordance with U.S. trade compliance laws and regulations. + +14.6 Government Rights. The Software, documentation and technology ("Protected Items") are "Commercial products" as this term is defined at 48 C.F.R. 2.101, consisting of "commercial computer software" and "commercial computer software documentation" as such terms are used in, respectively, 48 C.F.R. 12.212 and 48 C.F.R. 227.7202 & 252.227-7014(a)(1). Before any Protected Items are supplied to the U.S. Government, you will (i) inform the U.S. Government in writing that the Protected Items are and must be treated as commercial computer software and commercial computer software documentation developed at private expense; (ii) inform the U.S. Government that the Protected Items are provided subject to the terms of the Agreement; and (iii) mark the Protected Items as commercial computer software and commercial computer software documentation developed at private expense. In no event will you permit the U.S. Government to acquire rights in Protected Items beyond those specified in 48 C.F.R. 52.227-19(b)(1)-(2) or 252.227-7013(c) except as expressly approved by NVIDIA in writing. + +14.7 Notices. Please direct your legal notices or other correspondence to legalnotices@nvidia.com with a copy mailed to NVIDIA Corporation, 2788 San Tomas Expressway, Santa Clara, California 95051, United States of America, Attention: Legal Department. If NVIDIA needs to contact you, you consent to receive the notices by email and agree that such notices will satisfy any legal communication requirements. + +14.8 Severability. If a court of competent jurisdiction rules that a provision of this Agreement is unenforceable, that provision will be deemed modified to the extent necessary to make it enforceable and the remainder of this Agreement will continue in full force and effect. + +14.9 Construction. The headings in the Agreement are included solely for convenience and are not intended to affect the meaning or interpretation of the Agreement. As required by the context of the Agreement, the singular of a term includes the plural and vice versa. + +14.10 Amendment. Any amendment to this Agreement must be in writing and signed by authorized representatives of both parties. + +14.11 Entire Agreement. Regarding the subject matter of this Agreement, the parties agree that (a) this Agreement constitutes the entire and exclusive agreement between the parties and supersedes all prior and contemporaneous communications and (b) any additional or different terms or conditions, whether contained in purchase orders, order acknowledgments, invoices or otherwise, will not be binding and are null and void. + +(v. August 15, 2025) diff --git a/docs/licenses/dependencies/cycler-license.txt b/docs/licenses/dependencies/cycler-license.txt new file mode 100644 index 0000000..539c7c1 --- /dev/null +++ b/docs/licenses/dependencies/cycler-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2015, matplotlib project +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the matplotlib project nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/decorator-license.txt b/docs/licenses/dependencies/decorator-license.txt new file mode 100644 index 0000000..3e01d05 --- /dev/null +++ b/docs/licenses/dependencies/decorator-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2025, Michele Simionato +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/licenses/dependencies/dex-retargeting-license.txt b/docs/licenses/dependencies/dex-retargeting-license.txt new file mode 100644 index 0000000..673ea4b --- /dev/null +++ b/docs/licenses/dependencies/dex-retargeting-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Yuzhe Qin + +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/docker-pycreds-license.txt b/docs/licenses/dependencies/docker-pycreds-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/docker-pycreds-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/egl_probe-license.txt b/docs/licenses/dependencies/egl_probe-license.txt new file mode 100644 index 0000000..934eaa8 --- /dev/null +++ b/docs/licenses/dependencies/egl_probe-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Stanford Vision and Learning Lab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/licenses/dependencies/eigenpy-license.txt b/docs/licenses/dependencies/eigenpy-license.txt new file mode 100644 index 0000000..c9a52bd --- /dev/null +++ b/docs/licenses/dependencies/eigenpy-license.txt @@ -0,0 +1,26 @@ +BSD 2-Clause License + +Copyright (c) 2014-2020, CNRS +Copyright (c) 2018-2025, INRIA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/einops-license.txt b/docs/licenses/dependencies/einops-license.txt new file mode 100644 index 0000000..3a654e9 --- /dev/null +++ b/docs/licenses/dependencies/einops-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Alex Rogozhnikov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/licenses/dependencies/executing-license.txt b/docs/licenses/dependencies/executing-license.txt new file mode 100644 index 0000000..473e36e --- /dev/null +++ b/docs/licenses/dependencies/executing-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alex Hall + +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/filelock-license.txt b/docs/licenses/dependencies/filelock-license.txt new file mode 100644 index 0000000..cf1ab25 --- /dev/null +++ b/docs/licenses/dependencies/filelock-license.txt @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to diff --git a/docs/licenses/dependencies/flaky-license.txt b/docs/licenses/dependencies/flaky-license.txt new file mode 100644 index 0000000..167ec4d --- /dev/null +++ b/docs/licenses/dependencies/flaky-license.txt @@ -0,0 +1,166 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/docs/licenses/dependencies/flatdict-license.txt b/docs/licenses/dependencies/flatdict-license.txt new file mode 100644 index 0000000..b0e19d4 --- /dev/null +++ b/docs/licenses/dependencies/flatdict-license.txt @@ -0,0 +1,25 @@ +Copyright (c) 2013-2020 Gavin M. Roy +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/fonttools-license.txt b/docs/licenses/dependencies/fonttools-license.txt new file mode 100644 index 0000000..cc63390 --- /dev/null +++ b/docs/licenses/dependencies/fonttools-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Just van Rossum + +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/fsspec-license.txt b/docs/licenses/dependencies/fsspec-license.txt new file mode 100644 index 0000000..67590a5 --- /dev/null +++ b/docs/licenses/dependencies/fsspec-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Martin Durant +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/gitdb-license.txt b/docs/licenses/dependencies/gitdb-license.txt new file mode 100644 index 0000000..c4986ed --- /dev/null +++ b/docs/licenses/dependencies/gitdb-license.txt @@ -0,0 +1,42 @@ +Copyright (C) 2010, 2011 Sebastian Thiel and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of the GitDB project nor the names of +its contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Additional Licenses +------------------- +The files at +gitdb/test/fixtures/packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.idx +and +gitdb/test/fixtures/packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack +are licensed under GNU GPL as part of the git source repository, +see http://en.wikipedia.org/wiki/Git_%28software%29 for more information. + +They are not required for the actual operation, which is why they are not found +in the distribution package. diff --git a/docs/licenses/dependencies/grpcio-license.txt b/docs/licenses/dependencies/grpcio-license.txt new file mode 100644 index 0000000..b444849 --- /dev/null +++ b/docs/licenses/dependencies/grpcio-license.txt @@ -0,0 +1,609 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +----------------------------------------------------------- + +BSD 3-Clause License + +Copyright 2016, Google Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------- + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/docs/licenses/dependencies/gym-notices-license.txt b/docs/licenses/dependencies/gym-notices-license.txt new file mode 100644 index 0000000..96f1555 --- /dev/null +++ b/docs/licenses/dependencies/gym-notices-license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2018 The Python Packaging Authority + +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/gymnasium-license.txt b/docs/licenses/dependencies/gymnasium-license.txt new file mode 100644 index 0000000..ac10bf4 --- /dev/null +++ b/docs/licenses/dependencies/gymnasium-license.txt @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2016 OpenAI +Copyright (c) 2022 Farama Foundation + +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/hf-xet-license.txt b/docs/licenses/dependencies/hf-xet-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/hf-xet-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/hpp-fcl-license.txt b/docs/licenses/dependencies/hpp-fcl-license.txt new file mode 100644 index 0000000..c548a9f --- /dev/null +++ b/docs/licenses/dependencies/hpp-fcl-license.txt @@ -0,0 +1,34 @@ +Software License Agreement (BSD License) + + Copyright (c) 2008-2014, Willow Garage, Inc. + Copyright (c) 2014-2015, Open Source Robotics Foundation + Copyright (c) 2014-2023, CNRS + Copyright (c) 2018-2025, INRIA + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Open Source Robotics Foundation nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/huggingface-hub-license.txt b/docs/licenses/dependencies/huggingface-hub-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/huggingface-hub-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/hydra-license.txt b/docs/licenses/dependencies/hydra-license.txt new file mode 100644 index 0000000..b96dcb0 --- /dev/null +++ b/docs/licenses/dependencies/hydra-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, 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. diff --git a/docs/licenses/dependencies/idna-license.txt b/docs/licenses/dependencies/idna-license.txt new file mode 100644 index 0000000..47e7567 --- /dev/null +++ b/docs/licenses/dependencies/idna-license.txt @@ -0,0 +1,13 @@ +BSD 3-Clause License + +Copyright (c) 2013-2025, Kim Davies and contributors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/imageio-license.txt b/docs/licenses/dependencies/imageio-license.txt new file mode 100644 index 0000000..4719432 --- /dev/null +++ b/docs/licenses/dependencies/imageio-license.txt @@ -0,0 +1,23 @@ +Copyright (c) 2014-2022, imageio developers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/imageio_ffmpeg-license.txt b/docs/licenses/dependencies/imageio_ffmpeg-license.txt new file mode 100644 index 0000000..6d27cf6 --- /dev/null +++ b/docs/licenses/dependencies/imageio_ffmpeg-license.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2019-2025, imageio +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/imagesize-license.txt b/docs/licenses/dependencies/imagesize-license.txt new file mode 100644 index 0000000..b0df45f --- /dev/null +++ b/docs/licenses/dependencies/imagesize-license.txt @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright Β© 2016 Yoshiki Shibukawa + +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/iniconfig-license.txt b/docs/licenses/dependencies/iniconfig-license.txt new file mode 100644 index 0000000..46f4b28 --- /dev/null +++ b/docs/licenses/dependencies/iniconfig-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010 - 2023 Holger Krekel and others + +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/ipython-license.txt b/docs/licenses/dependencies/ipython-license.txt new file mode 100644 index 0000000..d4bb8d3 --- /dev/null +++ b/docs/licenses/dependencies/ipython-license.txt @@ -0,0 +1,33 @@ +BSD 3-Clause License + +- Copyright (c) 2008-Present, IPython Development Team +- Copyright (c) 2001-2007, Fernando Perez +- Copyright (c) 2001, Janko Hauser +- Copyright (c) 2001, Nathaniel Gray + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/ipywidgets-license.txt b/docs/licenses/dependencies/ipywidgets-license.txt new file mode 100644 index 0000000..deb2c38 --- /dev/null +++ b/docs/licenses/dependencies/ipywidgets-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/isaacsim-license.txt b/docs/licenses/dependencies/isaacsim-license.txt index 80cff4a..0454ece 100644 --- a/docs/licenses/dependencies/isaacsim-license.txt +++ b/docs/licenses/dependencies/isaacsim-license.txt @@ -1,13 +1,188 @@ -Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +The Isaac Sim software in this repository is covered under the Apache 2.0 +License terms below. -NVIDIA CORPORATION and its licensors retain all intellectual property -and proprietary rights in and to this software, related documentation -and any modifications thereto. Any use, reproduction, disclosure or -distribution of this software and related documentation without an express -license agreement from NVIDIA CORPORATION is strictly prohibited. +Building or using the software requires additional components licenced +under other terms. These additional components include dependencies such +as the Omniverse Kit SDK, as well as 3D models and textures. -Note: Licenses for assets such as Robots and Props used within these environments can be found inside their respective folders on the Nucleus server where they are hosted. +License terms for these additional NVIDIA owned and licensed components +can be found here: + https://docs.nvidia.com/NVIDIA-IsaacSim-Additional-Software-and-Materials-License.pdf -For more information: https://docs.omniverse.nvidia.com/app_isaacsim/common/NVIDIA_Omniverse_License_Agreement.html +Any open source dependencies downloaded during the build process are owned +by their respective owners and licensed under their respective terms. -For sub-dependencies of Isaac Sim: https://docs.omniverse.nvidia.com/app_isaacsim/common/licenses.html + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/docs/licenses/dependencies/jinja2-license.txt b/docs/licenses/dependencies/jinja2-license.txt new file mode 100644 index 0000000..c37cae4 --- /dev/null +++ b/docs/licenses/dependencies/jinja2-license.txt @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/jmespath-license.txt b/docs/licenses/dependencies/jmespath-license.txt new file mode 100644 index 0000000..9c520c6 --- /dev/null +++ b/docs/licenses/dependencies/jmespath-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 Amazon.com, Inc. or its affiliates. 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. diff --git a/docs/licenses/dependencies/joblib-license.txt b/docs/licenses/dependencies/joblib-license.txt new file mode 100644 index 0000000..910537b --- /dev/null +++ b/docs/licenses/dependencies/joblib-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2008-2021, The joblib developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/jsonschema-license.txt b/docs/licenses/dependencies/jsonschema-license.txt new file mode 100644 index 0000000..af9cfbd --- /dev/null +++ b/docs/licenses/dependencies/jsonschema-license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2013 Julian Berman + +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/jsonschema-specifications-license.txt b/docs/licenses/dependencies/jsonschema-specifications-license.txt new file mode 100644 index 0000000..a9f853e --- /dev/null +++ b/docs/licenses/dependencies/jsonschema-specifications-license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2022 Julian Berman + +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/junitparser-license.txt b/docs/licenses/dependencies/junitparser-license.txt new file mode 100644 index 0000000..a4325a4 --- /dev/null +++ b/docs/licenses/dependencies/junitparser-license.txt @@ -0,0 +1,13 @@ +Copyright 2020 Joel Wang + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/jupyterlab_widgets-license.txt b/docs/licenses/dependencies/jupyterlab_widgets-license.txt new file mode 100644 index 0000000..deb2c38 --- /dev/null +++ b/docs/licenses/dependencies/jupyterlab_widgets-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/kiwisolver-license.txt b/docs/licenses/dependencies/kiwisolver-license.txt new file mode 100644 index 0000000..206ae79 --- /dev/null +++ b/docs/licenses/dependencies/kiwisolver-license.txt @@ -0,0 +1,71 @@ +========================= + The Kiwi licensing terms +========================= +Kiwi is licensed under the terms of the Modified BSD License (also known as +New or Revised BSD), as follows: + +Copyright (c) 2013-2024, Nucleic Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the Nucleic Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +About Kiwi +---------- +Chris Colbert began the Kiwi project in December 2013 in an effort to +create a blisteringly fast UI constraint solver. Chris is still the +project lead. + +The Nucleic Development Team is the set of all contributors to the Nucleic +project and its subprojects. + +The core team that coordinates development on GitHub can be found here: +http://github.com/nucleic. The current team consists of: + +* Chris Colbert + +Our Copyright Policy +-------------------- +Nucleic uses a shared copyright model. Each contributor maintains copyright +over their contributions to Nucleic. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the Nucleic +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Nucleic +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Nucleic repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + +#------------------------------------------------------------------------------ +# Copyright (c) 2013-2024, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +#------------------------------------------------------------------------------ diff --git a/docs/licenses/dependencies/labeler-license.txt b/docs/licenses/dependencies/labeler-license.txt new file mode 100644 index 0000000..cfbc8bb --- /dev/null +++ b/docs/licenses/dependencies/labeler-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 GitHub, Inc. and contributors + +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/latexcodec-license.txt b/docs/licenses/dependencies/latexcodec-license.txt new file mode 100644 index 0000000..e9511f2 --- /dev/null +++ b/docs/licenses/dependencies/latexcodec-license.txt @@ -0,0 +1,7 @@ +latexcodec is a lexer and codec to work with LaTeX code in Python +Copyright (c) 2011-2020 by Matthias C. M. Troffaes +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/loop-rate-limiters-license.txt b/docs/licenses/dependencies/loop-rate-limiters-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/loop-rate-limiters-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/markdown-it-py-license.txt b/docs/licenses/dependencies/markdown-it-py-license.txt new file mode 100644 index 0000000..582ddf5 --- /dev/null +++ b/docs/licenses/dependencies/markdown-it-py-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 ExecutableBookProject + +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/markdown-license.txt b/docs/licenses/dependencies/markdown-license.txt new file mode 100644 index 0000000..ff993de --- /dev/null +++ b/docs/licenses/dependencies/markdown-license.txt @@ -0,0 +1,15 @@ +BSD 3-Clause License + +Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/markupsafe-license.txt b/docs/licenses/dependencies/markupsafe-license.txt new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/docs/licenses/dependencies/markupsafe-license.txt @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/matplotlib-inline-license.txt b/docs/licenses/dependencies/matplotlib-inline-license.txt new file mode 100644 index 0000000..b835037 --- /dev/null +++ b/docs/licenses/dependencies/matplotlib-inline-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019-2022, IPython Development Team. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/mdit-py-plugins-license.txt b/docs/licenses/dependencies/mdit-py-plugins-license.txt new file mode 100644 index 0000000..582ddf5 --- /dev/null +++ b/docs/licenses/dependencies/mdit-py-plugins-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 ExecutableBookProject + +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/mdurl-license.txt b/docs/licenses/dependencies/mdurl-license.txt new file mode 100644 index 0000000..2a920c5 --- /dev/null +++ b/docs/licenses/dependencies/mdurl-license.txt @@ -0,0 +1,46 @@ +Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin. +Copyright (c) 2021 Taneli Hukkinen + +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. + +-------------------------------------------------------------------------------- + +.parse() is based on Joyent's node.js `url` code: + +Copyright Joyent, Inc. and other Node contributors. 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. diff --git a/docs/licenses/dependencies/moviepy-license.txt b/docs/licenses/dependencies/moviepy-license.txt new file mode 100644 index 0000000..68c6e00 --- /dev/null +++ b/docs/licenses/dependencies/moviepy-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Zulko + +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/mpmath-license.txt b/docs/licenses/dependencies/mpmath-license.txt new file mode 100644 index 0000000..6aa2fc9 --- /dev/null +++ b/docs/licenses/dependencies/mpmath-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2025 Fredrik Johansson and mpmath contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/licenses/dependencies/myst-parser-license.txt b/docs/licenses/dependencies/myst-parser-license.txt new file mode 100644 index 0000000..582ddf5 --- /dev/null +++ b/docs/licenses/dependencies/myst-parser-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 ExecutableBookProject + +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/networkx-license.txt b/docs/licenses/dependencies/networkx-license.txt new file mode 100644 index 0000000..0bf9a8f --- /dev/null +++ b/docs/licenses/dependencies/networkx-license.txt @@ -0,0 +1,37 @@ +NetworkX is distributed with the 3-clause BSD license. + +:: + + Copyright (c) 2004-2025, NetworkX Developers + Aric Hagberg + Dan Schult + Pieter Swart + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NetworkX Developers nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/newton-license.txt b/docs/licenses/dependencies/newton-license.txt new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/docs/licenses/dependencies/newton-license.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/docs/licenses/dependencies/nlopt-license.txt b/docs/licenses/dependencies/nlopt-license.txt new file mode 100644 index 0000000..884683d --- /dev/null +++ b/docs/licenses/dependencies/nlopt-license.txt @@ -0,0 +1,39 @@ +NLopt combines several free/open-source nonlinear optimization +libraries by various authors. See the COPYING, COPYRIGHT, and README +files in the subdirectories for the original copyright and licensing +information of these packages. + +The compiled NLopt library, i.e. the combined work of all of the +included optimization routines, is licensed under the conjunction of +all of these licensing terms. By default, the most restrictive terms +are for the code in the "luksan" directory, which is licensed under +the GNU Lesser General Public License (GNU LGPL), version 2.1 or +later (see luksan/COPYRIGHT). That means that, by default, the compiled +NLopt library is governed by the terms of the LGPL. + +--------------------------------------------------------------------------- + +However, NLopt also offers the option to be built without the code in +the "luksan" directory. In this case, NLopt, including any modifications +to the abovementioned packages, are licensed under the standard "MIT License:" + +Copyright (c) 2007-2024 Massachusetts Institute of Technology + +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/octomap-license.txt b/docs/licenses/dependencies/octomap-license.txt new file mode 100644 index 0000000..a3c73b0 --- /dev/null +++ b/docs/licenses/dependencies/octomap-license.txt @@ -0,0 +1,31 @@ + +OctoMap - An Efficient Probabilistic 3D Mapping Framework Based on Octrees + +License for the "octomap" library: New BSD License. + +Copyright (c) 2009-2013, K.M. Wurm and A. Hornung, University of Freiburg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the University of Freiburg nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/omegaconf-license.txt b/docs/licenses/dependencies/omegaconf-license.txt new file mode 100644 index 0000000..eb208da --- /dev/null +++ b/docs/licenses/dependencies/omegaconf-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Omry Yadan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/opencv-python-license.txt b/docs/licenses/dependencies/opencv-python-license.txt new file mode 100644 index 0000000..09ad12a --- /dev/null +++ b/docs/licenses/dependencies/opencv-python-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Olli-Pekka Heinisuo + +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/packaging-license.txt b/docs/licenses/dependencies/packaging-license.txt new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/docs/licenses/dependencies/packaging-license.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/docs/licenses/dependencies/pandas-license.txt b/docs/licenses/dependencies/pandas-license.txt new file mode 100644 index 0000000..c343da2 --- /dev/null +++ b/docs/licenses/dependencies/pandas-license.txt @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team +All rights reserved. + +Copyright (c) 2011-2025, Open source contributors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pathtools-license.txt b/docs/licenses/dependencies/pathtools-license.txt new file mode 100644 index 0000000..5e6adc9 --- /dev/null +++ b/docs/licenses/dependencies/pathtools-license.txt @@ -0,0 +1,21 @@ +Copyright (C) 2010 by Yesudeep Mangalapilly + +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/pexpect-license.txt b/docs/licenses/dependencies/pexpect-license.txt new file mode 100644 index 0000000..6ee60f4 --- /dev/null +++ b/docs/licenses/dependencies/pexpect-license.txt @@ -0,0 +1,19 @@ +ISC LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2013-2014, Pexpect development team + Copyright (c) 2012, Noah Spurrier + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/licenses/dependencies/pin-license.txt b/docs/licenses/dependencies/pin-license.txt new file mode 100644 index 0000000..dfacb67 --- /dev/null +++ b/docs/licenses/dependencies/pin-license.txt @@ -0,0 +1,26 @@ +BSD 2-Clause License + +Copyright (c) 2014-2023, CNRS +Copyright (c) 2018-2025, INRIA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pin-pink-license.txt b/docs/licenses/dependencies/pin-pink-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/pin-pink-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/pinocchio-license.txt b/docs/licenses/dependencies/pinocchio-license.txt new file mode 100644 index 0000000..dfacb67 --- /dev/null +++ b/docs/licenses/dependencies/pinocchio-license.txt @@ -0,0 +1,26 @@ +BSD 2-Clause License + +Copyright (c) 2014-2023, CNRS +Copyright (c) 2018-2025, INRIA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pluggy-license.txt b/docs/licenses/dependencies/pluggy-license.txt new file mode 100644 index 0000000..85f4dd6 --- /dev/null +++ b/docs/licenses/dependencies/pluggy-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42) + +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/prettytable-license.txt b/docs/licenses/dependencies/prettytable-license.txt new file mode 100644 index 0000000..cb6fed3 --- /dev/null +++ b/docs/licenses/dependencies/prettytable-license.txt @@ -0,0 +1,30 @@ +# Copyright (c) 2009-2014 Luke Maurits +# All rights reserved. +# With contributions from: +# * Chris Clark +# * Klein Stephane +# * John Filleau +# * Vladimir VrziΔ‡ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/proglog-license.txt b/docs/licenses/dependencies/proglog-license.txt new file mode 100644 index 0000000..a68d0a5 --- /dev/null +++ b/docs/licenses/dependencies/proglog-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Edinburgh Genome Foundry, University of Edinburgh + +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/psutil-license.txt b/docs/licenses/dependencies/psutil-license.txt new file mode 100644 index 0000000..cff5eb7 --- /dev/null +++ b/docs/licenses/dependencies/psutil-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the psutil authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/ptyprocess-license.txt b/docs/licenses/dependencies/ptyprocess-license.txt new file mode 100644 index 0000000..6ee60f4 --- /dev/null +++ b/docs/licenses/dependencies/ptyprocess-license.txt @@ -0,0 +1,19 @@ +ISC LICENSE + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2013-2014, Pexpect development team + Copyright (c) 2012, Noah Spurrier + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/licenses/dependencies/pure_eval-license.txt b/docs/licenses/dependencies/pure_eval-license.txt new file mode 100644 index 0000000..473e36e --- /dev/null +++ b/docs/licenses/dependencies/pure_eval-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alex Hall + +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/pybtex-docutils-license.txt b/docs/licenses/dependencies/pybtex-docutils-license.txt new file mode 100644 index 0000000..810a5a8 --- /dev/null +++ b/docs/licenses/dependencies/pybtex-docutils-license.txt @@ -0,0 +1,7 @@ +pybtex-docutils is a docutils backend for pybtex +Copyright (c) 2013-2023 by Matthias C. M. Troffaes +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/pydata-sphinx-theme-license.txt b/docs/licenses/dependencies/pydata-sphinx-theme-license.txt new file mode 100644 index 0000000..464d117 --- /dev/null +++ b/docs/licenses/dependencies/pydata-sphinx-theme-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, pandas +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pygame-license.txt b/docs/licenses/dependencies/pygame-license.txt new file mode 100644 index 0000000..56de5de --- /dev/null +++ b/docs/licenses/dependencies/pygame-license.txt @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/docs/licenses/dependencies/pyglet-license.txt b/docs/licenses/dependencies/pyglet-license.txt new file mode 100644 index 0000000..03c0900 --- /dev/null +++ b/docs/licenses/dependencies/pyglet-license.txt @@ -0,0 +1,30 @@ +Copyright (c) 2006-2008 Alex Holkner +Copyright (c) 2008-2023 pyglet contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of pyglet nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pyparsing-license.txt b/docs/licenses/dependencies/pyparsing-license.txt new file mode 100644 index 0000000..1bf9852 --- /dev/null +++ b/docs/licenses/dependencies/pyparsing-license.txt @@ -0,0 +1,18 @@ +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/pyperclip-license.txt b/docs/licenses/dependencies/pyperclip-license.txt new file mode 100644 index 0000000..799b74c --- /dev/null +++ b/docs/licenses/dependencies/pyperclip-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2014, Al Sweigart +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pytest-license.txt b/docs/licenses/dependencies/pytest-license.txt new file mode 100644 index 0000000..c3f1657 --- /dev/null +++ b/docs/licenses/dependencies/pytest-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2004 Holger Krekel and others + +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/pytest-mock-license.txt b/docs/licenses/dependencies/pytest-mock-license.txt new file mode 100644 index 0000000..6e1ee5d --- /dev/null +++ b/docs/licenses/dependencies/pytest-mock-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2016] [Bruno Oliveira] + +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/python-dateutil-license.txt b/docs/licenses/dependencies/python-dateutil-license.txt new file mode 100644 index 0000000..6053d35 --- /dev/null +++ b/docs/licenses/dependencies/python-dateutil-license.txt @@ -0,0 +1,54 @@ +Copyright 2017- Paul Ganssle +Copyright 2017- dateutil contributors (see AUTHORS file) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The above license applies to all contributions after 2017-12-01, as well as +all contributions that have been re-licensed (see AUTHORS file for the list of +contributors who have re-licensed their code). +-------------------------------------------------------------------------------- +dateutil - Extensions to the standard Python datetime module. + +Copyright (c) 2003-2011 - Gustavo Niemeyer +Copyright (c) 2012-2014 - Tomi PievilΓ€inen +Copyright (c) 2014-2016 - Yaron de Leeuw +Copyright (c) 2015- - Paul Ganssle +Copyright (c) 2015- - dateutil contributors (see AUTHORS file) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The above BSD License Applies to all code, even that also covered by Apache 2.0. diff --git a/docs/licenses/dependencies/python-dotenv-license.txt b/docs/licenses/dependencies/python-dotenv-license.txt new file mode 100644 index 0000000..3a97119 --- /dev/null +++ b/docs/licenses/dependencies/python-dotenv-license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv) + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of django-dotenv nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pytransform3d-license.txt b/docs/licenses/dependencies/pytransform3d-license.txt new file mode 100644 index 0000000..061bfc4 --- /dev/null +++ b/docs/licenses/dependencies/pytransform3d-license.txt @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright 2014-2017 Alexander Fabisch (Robotics Research Group, University of Bremen), +2017-2025 Alexander Fabisch (DFKI GmbH, Robotics Innovation Center), +and pytransform3d contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pytz-license.txt b/docs/licenses/dependencies/pytz-license.txt new file mode 100644 index 0000000..5f1c112 --- /dev/null +++ b/docs/licenses/dependencies/pytz-license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2003-2019 Stuart Bishop + +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/pyyaml-license.txt b/docs/licenses/dependencies/pyyaml-license.txt new file mode 100644 index 0000000..2f1b8e1 --- /dev/null +++ b/docs/licenses/dependencies/pyyaml-license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2017-2021 Ingy dΓΆt Net +Copyright (c) 2006-2016 Kirill Simonov + +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/qhull-license.txt b/docs/licenses/dependencies/qhull-license.txt new file mode 100644 index 0000000..9c802c0 --- /dev/null +++ b/docs/licenses/dependencies/qhull-license.txt @@ -0,0 +1,39 @@ + Qhull, Copyright (c) 1993-2020 + + C.B. Barber + Arlington, MA + + and + + The National Science and Technology Research Center for + Computation and Visualization of Geometric Structures + (The Geometry Center) + University of Minnesota + + email: qhull@qhull.org + +This software includes Qhull from C.B. Barber and The Geometry Center. +Files derived from Qhull 1.0 are copyrighted by the Geometry Center. The +remaining files are copyrighted by C.B. Barber. Qhull is free software +and may be obtained via http from www.qhull.org. It may be freely copied, +modified, and redistributed under the following conditions: + +1. All copyright notices must remain intact in all files. + +2. A copy of this text file must be distributed along with any copies + of Qhull that you redistribute; this includes copies that you have + modified, or copies of programs or other software products that + include Qhull. + +3. If you modify Qhull, you must include a notice giving the + name of the person performing the modification, the date of + modification, and the reason for such modification. + +4. When distributing modified versions of Qhull, or other software + products that include Qhull, you must provide notice that the original + source code may be obtained as noted above. + +5. There is no warranty or other guarantee of fitness for Qhull, it is + provided solely "as is". Bug reports or fixes may be sent to + qhull_bug@qhull.org; the authors may or may not act on them as + they desire. diff --git a/docs/licenses/dependencies/qpsolvers-license.txt b/docs/licenses/dependencies/qpsolvers-license.txt new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/docs/licenses/dependencies/qpsolvers-license.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/docs/licenses/dependencies/quadprog-license.txt b/docs/licenses/dependencies/quadprog-license.txt new file mode 100644 index 0000000..23cb790 --- /dev/null +++ b/docs/licenses/dependencies/quadprog-license.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/docs/licenses/dependencies/referencing-license.txt b/docs/licenses/dependencies/referencing-license.txt new file mode 100644 index 0000000..a9f853e --- /dev/null +++ b/docs/licenses/dependencies/referencing-license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2022 Julian Berman + +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/regex-license.txt b/docs/licenses/dependencies/regex-license.txt new file mode 100644 index 0000000..99c19cf --- /dev/null +++ b/docs/licenses/dependencies/regex-license.txt @@ -0,0 +1,208 @@ +This work was derived from the 're' module of CPython 2.6 and CPython 3.1, +copyright (c) 1998-2001 by Secret Labs AB and licensed under CNRI's Python 1.6 +license. + +All additions and alterations are licensed under the Apache 2.0 License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Matthew Barnett + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/requests-license.txt b/docs/licenses/dependencies/requests-license.txt new file mode 100644 index 0000000..dd5b3a5 --- /dev/null +++ b/docs/licenses/dependencies/requests-license.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/docs/licenses/dependencies/rich-license.txt b/docs/licenses/dependencies/rich-license.txt new file mode 100644 index 0000000..4415505 --- /dev/null +++ b/docs/licenses/dependencies/rich-license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2020 Will McGugan + +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/safetensors-license.txt b/docs/licenses/dependencies/safetensors-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/safetensors-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/scikit-learn-license.txt b/docs/licenses/dependencies/scikit-learn-license.txt new file mode 100644 index 0000000..e1cd01d --- /dev/null +++ b/docs/licenses/dependencies/scikit-learn-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2007-2024 The scikit-learn developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sentry-sdk-license.txt b/docs/licenses/dependencies/sentry-sdk-license.txt new file mode 100644 index 0000000..016323b --- /dev/null +++ b/docs/licenses/dependencies/sentry-sdk-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Functional Software, Inc. dba Sentry + +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/setpruwtle-license.txt b/docs/licenses/dependencies/setpruwtle-license.txt new file mode 100644 index 0000000..1f632fa --- /dev/null +++ b/docs/licenses/dependencies/setpruwtle-license.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2009, Daniele Varrazzo + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/setuptools-license.txt b/docs/licenses/dependencies/setuptools-license.txt new file mode 100644 index 0000000..1bb5a44 --- /dev/null +++ b/docs/licenses/dependencies/setuptools-license.txt @@ -0,0 +1,17 @@ +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/shortuuid-license.txt b/docs/licenses/dependencies/shortuuid-license.txt new file mode 100644 index 0000000..c14f01b --- /dev/null +++ b/docs/licenses/dependencies/shortuuid-license.txt @@ -0,0 +1,29 @@ +Copyright (c) 2011, Stavros Korokithakis +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +Neither the name of Stochastic Technologies nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/six-license.txt b/docs/licenses/dependencies/six-license.txt new file mode 100644 index 0000000..1cc22a5 --- /dev/null +++ b/docs/licenses/dependencies/six-license.txt @@ -0,0 +1,18 @@ +Copyright (c) 2010-2024 Benjamin Peterson + +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/smmap-license.txt b/docs/licenses/dependencies/smmap-license.txt new file mode 100644 index 0000000..8ef91f9 --- /dev/null +++ b/docs/licenses/dependencies/smmap-license.txt @@ -0,0 +1,29 @@ +Copyright (C) 2010, 2011 Sebastian Thiel and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of the async project nor the names of +its contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sniffio-license.txt b/docs/licenses/dependencies/sniffio-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/sniffio-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/snowballstemmer-license.txt b/docs/licenses/dependencies/snowballstemmer-license.txt new file mode 100644 index 0000000..f36607f --- /dev/null +++ b/docs/licenses/dependencies/snowballstemmer-license.txt @@ -0,0 +1,29 @@ +Copyright (c) 2001, Dr Martin Porter +Copyright (c) 2004,2005, Richard Boulton +Copyright (c) 2013, Yoshiki Shibukawa +Copyright (c) 2006,2007,2009,2010,2011,2014-2019, Olly Betts +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of the Snowball project nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/soupsieve-license.txt b/docs/licenses/dependencies/soupsieve-license.txt new file mode 100644 index 0000000..a34ec87 --- /dev/null +++ b/docs/licenses/dependencies/soupsieve-license.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2018 - 2025 Isaac Muse isaacmuse@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/licenses/dependencies/sphinx-book-theme-license.txt b/docs/licenses/dependencies/sphinx-book-theme-license.txt new file mode 100644 index 0000000..462817b --- /dev/null +++ b/docs/licenses/dependencies/sphinx-book-theme-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Chris Holdgraf +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinx-copybutton-license.txt b/docs/licenses/dependencies/sphinx-copybutton-license.txt new file mode 100644 index 0000000..dab6ba4 --- /dev/null +++ b/docs/licenses/dependencies/sphinx-copybutton-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Chris Holdgraf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/licenses/dependencies/sphinx-design-license.txt b/docs/licenses/dependencies/sphinx-design-license.txt new file mode 100644 index 0000000..d6bd96e --- /dev/null +++ b/docs/licenses/dependencies/sphinx-design-license.txt @@ -0,0 +1,21 @@ +MIT License Copyright (c) 2023 Chris Sewell + +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 +(including the next paragraph) shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/licenses/dependencies/sphinx-icon-license.txt b/docs/licenses/dependencies/sphinx-icon-license.txt new file mode 100644 index 0000000..b17ba32 --- /dev/null +++ b/docs/licenses/dependencies/sphinx-icon-license.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2022, Rambaud Pierrick +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinx-license.txt b/docs/licenses/dependencies/sphinx-license.txt new file mode 100644 index 0000000..de3688c --- /dev/null +++ b/docs/licenses/dependencies/sphinx-license.txt @@ -0,0 +1,31 @@ +License for Sphinx +================== + +Unless otherwise indicated, all code in the Sphinx project is licenced under the +two clause BSD licence below. + +Copyright (c) 2007-2025 by the Sphinx team (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinx-tabs-license.txt b/docs/licenses/dependencies/sphinx-tabs-license.txt new file mode 100644 index 0000000..c124ccd --- /dev/null +++ b/docs/licenses/dependencies/sphinx-tabs-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 djungelorm + +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/sphinxcontrib-applehelp-license.txt b/docs/licenses/dependencies/sphinxcontrib-applehelp-license.txt new file mode 100644 index 0000000..8391fd1 --- /dev/null +++ b/docs/licenses/dependencies/sphinxcontrib-applehelp-license.txt @@ -0,0 +1,8 @@ +License for sphinxcontrib-applehelp +Copyright (c) 2007-2019 by the Sphinx team (see https://github.com/sphinx-doc/sphinx/blob/master/AUTHORS). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinxcontrib-bibtex-license.txt b/docs/licenses/dependencies/sphinxcontrib-bibtex-license.txt new file mode 100644 index 0000000..fb3f169 --- /dev/null +++ b/docs/licenses/dependencies/sphinxcontrib-bibtex-license.txt @@ -0,0 +1,26 @@ +| sphinxcontrib-bibtex is a Sphinx extension for BibTeX style citations +| Copyright (c) 2011-2024 by Matthias C. M. Troffaes +| All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinxcontrib-devhelp-license.txt b/docs/licenses/dependencies/sphinxcontrib-devhelp-license.txt new file mode 100644 index 0000000..e15238f --- /dev/null +++ b/docs/licenses/dependencies/sphinxcontrib-devhelp-license.txt @@ -0,0 +1,8 @@ +License for sphinxcontrib-devhelp +Copyright (c) 2007-2019 by the Sphinx team (see https://github.com/sphinx-doc/sphinx/blob/master/AUTHORS). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinxcontrib-htmlhelp-license.txt b/docs/licenses/dependencies/sphinxcontrib-htmlhelp-license.txt new file mode 100644 index 0000000..0339165 --- /dev/null +++ b/docs/licenses/dependencies/sphinxcontrib-htmlhelp-license.txt @@ -0,0 +1,8 @@ +License for sphinxcontrib-htmlhelp +Copyright (c) 2007-2019 by the Sphinx team (see https://github.com/sphinx-doc/sphinx/blob/master/AUTHORS). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinxcontrib-jsmath-license.txt b/docs/licenses/dependencies/sphinxcontrib-jsmath-license.txt new file mode 100644 index 0000000..e28495c --- /dev/null +++ b/docs/licenses/dependencies/sphinxcontrib-jsmath-license.txt @@ -0,0 +1,8 @@ +License for sphinxcontrib-jsmath +Copyright (c) 2007-2019 by the Sphinx team (see https://github.com/sphinx-doc/sphinx/blob/master/AUTHORS). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinxcontrib-qthelp-license.txt b/docs/licenses/dependencies/sphinxcontrib-qthelp-license.txt new file mode 100644 index 0000000..dd8f49e --- /dev/null +++ b/docs/licenses/dependencies/sphinxcontrib-qthelp-license.txt @@ -0,0 +1,8 @@ +License for sphinxcontrib-qthelp +Copyright (c) 2007-2019 by the Sphinx team (see https://github.com/sphinx-doc/sphinx/blob/master/AUTHORS). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinxcontrib-serializinghtml-license.txt b/docs/licenses/dependencies/sphinxcontrib-serializinghtml-license.txt new file mode 100644 index 0000000..b396b73 --- /dev/null +++ b/docs/licenses/dependencies/sphinxcontrib-serializinghtml-license.txt @@ -0,0 +1,8 @@ +License for sphinxcontrib-serializinghtml +Copyright (c) 2007-2019 by the Sphinx team (see https://github.com/sphinx-doc/sphinx/blob/master/AUTHORS). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/sphinxemoji-license.txt b/docs/licenses/dependencies/sphinxemoji-license.txt new file mode 100644 index 0000000..6d6be19 --- /dev/null +++ b/docs/licenses/dependencies/sphinxemoji-license.txt @@ -0,0 +1,27 @@ +Copyright (c) The Sphinx Emoji Codes contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Markdownreveal nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/stable-baselines3-license.txt b/docs/licenses/dependencies/stable-baselines3-license.txt new file mode 100644 index 0000000..0951e29 --- /dev/null +++ b/docs/licenses/dependencies/stable-baselines3-license.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2019 Antonin Raffin + +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/stack-data-license.txt b/docs/licenses/dependencies/stack-data-license.txt new file mode 100644 index 0000000..473e36e --- /dev/null +++ b/docs/licenses/dependencies/stack-data-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alex Hall + +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/sympy-license.txt b/docs/licenses/dependencies/sympy-license.txt new file mode 100644 index 0000000..bff6f8f --- /dev/null +++ b/docs/licenses/dependencies/sympy-license.txt @@ -0,0 +1,153 @@ +Copyright (c) 2006-2023 SymPy Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of SymPy nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +Patches that were taken from the Diofant project (https://github.com/diofant/diofant) +are licensed as: + +Copyright (c) 2006-2018 SymPy Development Team, + 2013-2023 Sergey B Kirpichev + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of Diofant or SymPy nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +Submodules taken from the multipledispatch project (https://github.com/mrocklin/multipledispatch) +are licensed as: + +Copyright (c) 2014 Matthew Rocklin + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of multipledispatch nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +The files under the directory sympy/parsing/autolev/tests/pydy-example-repo +are directly copied from PyDy project and are licensed as: + +Copyright (c) 2009-2023, PyDy Authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL PYDY AUTHORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files under the directory sympy/parsing/latex +are directly copied from latex2sympy project and are licensed as: + +Copyright 2016, latex2sympy + +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/tensorboardx-license.txt b/docs/licenses/dependencies/tensorboardx-license.txt new file mode 100644 index 0000000..bed02cd --- /dev/null +++ b/docs/licenses/dependencies/tensorboardx-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Tzu-Wei Huang + +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/termcolor-license.txt b/docs/licenses/dependencies/termcolor-license.txt new file mode 100644 index 0000000..d0b7970 --- /dev/null +++ b/docs/licenses/dependencies/termcolor-license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2008-2011 Volvox Development 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. diff --git a/docs/licenses/dependencies/threadpoolctl-license.txt b/docs/licenses/dependencies/threadpoolctl-license.txt new file mode 100644 index 0000000..1d7302b --- /dev/null +++ b/docs/licenses/dependencies/threadpoolctl-license.txt @@ -0,0 +1,24 @@ +Copyright (c) 2019, threadpoolctl contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/tinyxml-license.txt b/docs/licenses/dependencies/tinyxml-license.txt new file mode 100644 index 0000000..85a6a36 --- /dev/null +++ b/docs/licenses/dependencies/tinyxml-license.txt @@ -0,0 +1,18 @@ +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must +not claim that you wrote the original software. If you use this +software in a product, an acknowledgment in the product documentation +would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source +distribution. diff --git a/docs/licenses/dependencies/tokenizers-license.txt b/docs/licenses/dependencies/tokenizers-license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/docs/licenses/dependencies/tokenizers-license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/toml-license.txt b/docs/licenses/dependencies/toml-license.txt new file mode 100644 index 0000000..576d83e --- /dev/null +++ b/docs/licenses/dependencies/toml-license.txt @@ -0,0 +1,27 @@ +The MIT License + +Copyright 2013-2019 William Pearson +Copyright 2015-2016 Julien Enselme +Copyright 2016 Google Inc. +Copyright 2017 Samuel Vasko +Copyright 2017 Nate Prewitt +Copyright 2017 Jack Evans +Copyright 2019 Filippo Broggini + +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/torchvision-license.txt b/docs/licenses/dependencies/torchvision-license.txt new file mode 100644 index 0000000..f5e3ec9 --- /dev/null +++ b/docs/licenses/dependencies/torchvision-license.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) Soumith Chintala 2016, +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/tqdm-license.txt b/docs/licenses/dependencies/tqdm-license.txt new file mode 100644 index 0000000..1899a73 --- /dev/null +++ b/docs/licenses/dependencies/tqdm-license.txt @@ -0,0 +1,49 @@ +`tqdm` is a product of collaborative work. +Unless otherwise stated, all authors (see commit logs) retain copyright +for their respective work, and release the work under the MIT licence +(text below). + +Exceptions or notable authors are listed below +in reverse chronological order: + +* files: * + MPL-2.0 2015-2024 (c) Casper da Costa-Luis + [casperdcl](https://github.com/casperdcl). +* files: tqdm/_tqdm.py + MIT 2016 (c) [PR #96] on behalf of Google Inc. +* files: tqdm/_tqdm.py README.rst .gitignore + MIT 2013 (c) Noam Yorav-Raphael, original author. + +[PR #96]: #96 + + +Mozilla Public Licence (MPL) v. 2.0 - Exhibit A +----------------------------------------------- + +This Source Code Form is subject to the terms of the +Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this project, +You can obtain one at https://mozilla.org/MPL/2.0/. + + +MIT License (MIT) +----------------- + +Copyright (c) 2013 noamraph + +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/traitlets-license.txt b/docs/licenses/dependencies/traitlets-license.txt new file mode 100644 index 0000000..76910b0 --- /dev/null +++ b/docs/licenses/dependencies/traitlets-license.txt @@ -0,0 +1,30 @@ +BSD 3-Clause License + +- Copyright (c) 2001-, IPython Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/transformers-license.txt b/docs/licenses/dependencies/transformers-license.txt new file mode 100644 index 0000000..68b7d66 --- /dev/null +++ b/docs/licenses/dependencies/transformers-license.txt @@ -0,0 +1,203 @@ +Copyright 2018- The Hugging Face team. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/licenses/dependencies/trimesh-license.txt b/docs/licenses/dependencies/trimesh-license.txt new file mode 100644 index 0000000..d057112 --- /dev/null +++ b/docs/licenses/dependencies/trimesh-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Michael Dawson-Haggerty + +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/triton-license.txt b/docs/licenses/dependencies/triton-license.txt new file mode 100644 index 0000000..1d0238e --- /dev/null +++ b/docs/licenses/dependencies/triton-license.txt @@ -0,0 +1,23 @@ +/* +* Copyright 2018-2020 Philippe Tillet +* Copyright 2020-2022 OpenAI +* +* 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/typing-extensions-license.txt b/docs/licenses/dependencies/typing-extensions-license.txt new file mode 100644 index 0000000..f26bcf4 --- /dev/null +++ b/docs/licenses/dependencies/typing-extensions-license.txt @@ -0,0 +1,279 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/licenses/dependencies/typing-inspection-license.txt b/docs/licenses/dependencies/typing-inspection-license.txt new file mode 100644 index 0000000..e825ad5 --- /dev/null +++ b/docs/licenses/dependencies/typing-inspection-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Pydantic Services Inc. 2025 to present + +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/tzdata-license.txt b/docs/licenses/dependencies/tzdata-license.txt new file mode 100644 index 0000000..c2f84ae --- /dev/null +++ b/docs/licenses/dependencies/tzdata-license.txt @@ -0,0 +1,15 @@ +Apache Software License 2.0 + +Copyright (c) 2020, Paul Ganssle (Google) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/docs/licenses/dependencies/urdfdom-license.txt b/docs/licenses/dependencies/urdfdom-license.txt new file mode 100644 index 0000000..4c328b8 --- /dev/null +++ b/docs/licenses/dependencies/urdfdom-license.txt @@ -0,0 +1,33 @@ +/********************************************************************* +* Software License Agreement (BSD License) +* +* Copyright (c) 2008, Willow Garage, Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions +* are met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above +* copyright notice, this list of conditions and the following +* disclaimer in the documentation and/or other materials provided +* with the distribution. +* * Neither the name of the Willow Garage nor the names of its +* contributors may be used to endorse or promote products derived +* from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +* POSSIBILITY OF SUCH DAMAGE. +*********************************************************************/ diff --git a/docs/licenses/dependencies/urllib3-license.txt b/docs/licenses/dependencies/urllib3-license.txt new file mode 100644 index 0000000..e6183d0 --- /dev/null +++ b/docs/licenses/dependencies/urllib3-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2008-2020 Andrey Petrov and contributors. + +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/uv-license.txt b/docs/licenses/dependencies/uv-license.txt new file mode 100644 index 0000000..0148351 --- /dev/null +++ b/docs/licenses/dependencies/uv-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 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/licenses/dependencies/wandb-license.txt b/docs/licenses/dependencies/wandb-license.txt new file mode 100644 index 0000000..1f4e645 --- /dev/null +++ b/docs/licenses/dependencies/wandb-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Weights and Biases, 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/licenses/dependencies/warp-license.txt b/docs/licenses/dependencies/warp-license.txt index 9fd8e1d..d9a10c0 100644 --- a/docs/licenses/dependencies/warp-license.txt +++ b/docs/licenses/dependencies/warp-license.txt @@ -1,36 +1,176 @@ -# NVIDIA Source Code License for Warp + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -## 1. Definitions + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -β€œLicensor” means any person or entity that distributes its Work. -β€œSoftware” means the original work of authorship made available under this License. -β€œWork” means the Software and any additions to or derivative works of the Software that are made available under this License. -The terms β€œreproduce,” β€œreproduction,” β€œderivative works,” and β€œdistribution” have the meaning as provided under U.S. copyright law; provided, however, that for the purposes of this License, derivative works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work. -Works, including the Software, are β€œmade available” under this License by including in or with the Work either (a) a copyright notice referencing the applicability of this License to the Work, or (b) a copy of this License. + 1. Definitions. -## 2. License Grant + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -2.1 Copyright Grant. Subject to the terms and conditions of this License, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free, copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense and distribute its Work and any resulting derivative works in any form. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -## 3. Limitations + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -3.1 Redistribution. You may reproduce or distribute the Work only if (a) you do so under this License, (b) you include a complete copy of this License with your distribution, and (c) you retain without modification any copyright, patent, trademark, or attribution notices that are present in the Work. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -3.2 Derivative Works. You may specify that additional or different terms apply to the use, reproduction, and distribution of your derivative works of the Work (β€œYour Terms”) only if (a) Your Terms provide that the use limitation in Section 3.3 applies to your derivative works, and (b) you identify the specific derivative works that are subject to Your Terms. Notwithstanding Your Terms, this License (including the redistribution requirements in Section 3.1) will continue to apply to the Work itself. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -3.3 Use Limitation. The Work and any derivative works thereof only may be used or intended for use non-commercially. Notwithstanding the foregoing, NVIDIA and its affiliates may use the Work and any derivative works commercially. As used herein, β€œnon-commercially” means for research or evaluation purposes only. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -3.4 Patent Claims. If you bring or threaten to bring a patent claim against any Licensor (including any claim, cross-claim or counterclaim in a lawsuit) to enforce any patents that you allege are infringed by any Work, then your rights under this License from such Licensor (including the grant in Section 2.1) will terminate immediately. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -3.5 Trademarks. This License does not grant any rights to use any Licensor’s or its affiliates’ names, logos, or trademarks, except as necessary to reproduce the notices described in this License. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -3.6 Termination. If you violate any term of this License, then your rights under this License (including the grant in Section 2.1) will terminate immediately. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -## 4. Disclaimer of Warranty. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -THE WORK IS PROVIDED β€œAS IS” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER THIS LICENSE. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -## 5. Limitation of Liability. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/docs/licenses/dependencies/werkzeug-license.txt b/docs/licenses/dependencies/werkzeug-license.txt new file mode 100644 index 0000000..c37cae4 --- /dev/null +++ b/docs/licenses/dependencies/werkzeug-license.txt @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/widgetsnbextension-license.txt b/docs/licenses/dependencies/widgetsnbextension-license.txt new file mode 100644 index 0000000..2ec51d7 --- /dev/null +++ b/docs/licenses/dependencies/widgetsnbextension-license.txt @@ -0,0 +1,27 @@ +BSD-3-Clause license +Copyright (c) 2015-2022, conda-forge contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/licenses/dependencies/zipp-license.txt b/docs/licenses/dependencies/zipp-license.txt new file mode 100644 index 0000000..b5b7b52 --- /dev/null +++ b/docs/licenses/dependencies/zipp-license.txt @@ -0,0 +1 @@ +license = "MIT" as indicated in pyproject.toml diff --git a/docs/licenses/dependencies/zlib-license.txt b/docs/licenses/dependencies/zlib-license.txt new file mode 100644 index 0000000..038d398 --- /dev/null +++ b/docs/licenses/dependencies/zlib-license.txt @@ -0,0 +1,25 @@ +/* zlib.h -- interface of the 'zlib' general purpose compression library + version 1.3.1, January 22nd, 2024 + + Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +*/ diff --git a/docs/make.bat b/docs/make.bat index 29fcc2e..676a3ab 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -13,8 +13,8 @@ if "%1" == "multi-docs" ( if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-multiversion ) - %SPHINXBUILD% >NUL 2>NUL - if errorlevel 9009 ( + where %SPHINXBUILD% >NUL 2>NUL + if errorlevel 1 ( echo. echo.The 'sphinx-multiversion' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point @@ -37,8 +37,8 @@ if "%1" == "current-docs" ( if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) - %SPHINXBUILD% >NUL 2>NUL - if errorlevel 9009 ( + where %SPHINXBUILD% >NUL 2>NUL + if errorlevel 1 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point @@ -49,7 +49,8 @@ if "%1" == "current-docs" ( echo.http://sphinx-doc.org/ exit /b 1 ) - %SPHINXBUILD% %SOURCEDIR% %BUILDDIR%\current %SPHINXOPTS% %O% + if exist "%BUILDDIR%\current" rmdir /s /q "%BUILDDIR%\current" + %SPHINXBUILD% -W "%SOURCEDIR%" "%BUILDDIR%\current" %SPHINXOPTS% goto end ) diff --git a/docs/source/_static/UW-logo-black.png b/docs/source/_static/UW-logo-black.png index ab6bda5..c9878ed 100644 Binary files a/docs/source/_static/UW-logo-black.png and b/docs/source/_static/UW-logo-black.png differ diff --git a/docs/source/_static/UW-logo-white.png b/docs/source/_static/UW-logo-white.png index 91b7abe..c5a0c58 100644 Binary files a/docs/source/_static/UW-logo-white.png and b/docs/source/_static/UW-logo-white.png differ diff --git a/docs/source/_static/actuator-group/actuator-dark.svg b/docs/source/_static/actuator-group/actuator-dark.svg deleted file mode 100644 index b9e0682..0000000 --- a/docs/source/_static/actuator-group/actuator-dark.svg +++ /dev/null @@ -1,522 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - DC Motor - Actuator Net(MLP/LSTM) - - Gripper - - - Arm - Base - Mimic Group - - - - open/close (1) - joint position(6) - joint position(12) - joint torque(12) - joint torque(6) - joint velocity(6) - - Simulation - - - - Actions - - - - - - - - - - - diff --git a/docs/source/_static/actuator-group/actuator-light.svg b/docs/source/_static/actuator-group/actuator-light.svg deleted file mode 100644 index 214b5a7..0000000 --- a/docs/source/_static/actuator-group/actuator-light.svg +++ /dev/null @@ -1,10214 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - DC Motor - Actuator Net(MLP/LSTM) - - Gripper - - - Arm - Base - Mimic Group - - - - open/close (1) - joint position(6) - joint position(12) - joint torque(12) - joint torque(6) - joint velocity(6) - - Simulation - - - - Actions - - - - - - - - - - - diff --git a/docs/source/_static/cover.png b/docs/source/_static/cover.png deleted file mode 100644 index 56a2ae3..0000000 --- a/docs/source/_static/cover.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1f95e26072f91e8d98fab031b2e5445b179a2c39652f552c96dbb803d30cfad -size 1154561 diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css index 3a76e12..7f4f160 100644 --- a/docs/source/_static/css/custom.css +++ b/docs/source/_static/css/custom.css @@ -2,26 +2,6 @@ * For reference: https://pydata-sphinx-theme.readthedocs.io/en/v0.9.0/user_guide/customizing.html * For colors: https://clrs.cc/ */ - @import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@400;700&display=swap'); - - body { - font-family: 'Josefin Sans', sans-serif; - } - - /* Apply the font to headings */ - h1, h2, h3, h4, h5, h6 { - font-family: 'Josefin Sans', sans-serif; - font-weight: 100; /* Adjust weight for headings if desired */ - } - -/* Style footnotes or smaller text if needed */ -.note { - font-size: 0.80em; /* Smaller font size */ - padding: 8px; /* Less padding around the note */ - margin-top: 10px; - border: 1px solid #ddd; /* Lighter border for subtlety */ - background-color: #f9f9f9; /* Softer background color */ -} /* anything related to the light theme */ html[data-theme="light"] { @@ -102,3 +82,31 @@ a { padding-top: 0.0rem !important; padding-bottom: 0.0rem !important; } + +/* showcase task tables */ + +.showcase-table { + min-width: 75%; +} + +.showcase-table td { + border-color: gray; + border-style: solid; + border-width: 1px; +} + +.showcase-table p { + margin: 0; + padding: 0; +} + +.showcase-table .rot90 { + transform: rotate(-90deg); + margin: 0; + padding: 0; +} + +.showcase-table .center { + text-align: center; + vertical-align: middle; +} diff --git a/docs/source/_static/multi-gpu-rl/a3c-dark.svg b/docs/source/_static/multi-gpu-rl/a3c-dark.svg deleted file mode 100644 index 48cdb84..0000000 --- a/docs/source/_static/multi-gpu-rl/a3c-dark.svg +++ /dev/null @@ -1,364 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/source/_static/multi-gpu-rl/a3c-light.svg b/docs/source/_static/multi-gpu-rl/a3c-light.svg deleted file mode 100644 index d24bfc2..0000000 --- a/docs/source/_static/multi-gpu-rl/a3c-light.svg +++ /dev/null @@ -1,385 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/source/_static/publications/omnireset/drawer_assembly_training_curve.jpg b/docs/source/_static/publications/omnireset/drawer_assembly_training_curve.jpg new file mode 100644 index 0000000..13d2c24 Binary files /dev/null and b/docs/source/_static/publications/omnireset/drawer_assembly_training_curve.jpg differ diff --git a/docs/source/_static/publications/omnireset/peg_training_curve.jpg b/docs/source/_static/publications/omnireset/peg_training_curve.jpg new file mode 100644 index 0000000..7ba4777 Binary files /dev/null and b/docs/source/_static/publications/omnireset/peg_training_curve.jpg differ diff --git a/docs/source/_static/publications/omnireset/twisting_training_curve.jpg b/docs/source/_static/publications/omnireset/twisting_training_curve.jpg new file mode 100644 index 0000000..26e83a1 Binary files /dev/null and b/docs/source/_static/publications/omnireset/twisting_training_curve.jpg differ diff --git a/docs/source/_static/publications/pg1/pg1.png b/docs/source/_static/publications/pg1/pg1.png index fe344ad..4cae874 100644 Binary files a/docs/source/_static/publications/pg1/pg1.png and b/docs/source/_static/publications/pg1/pg1.png differ diff --git a/docs/source/_static/refs.bib b/docs/source/_static/refs.bib index e7f82d1..c3c3819 100644 --- a/docs/source/_static/refs.bib +++ b/docs/source/_static/refs.bib @@ -100,7 +100,7 @@ @ARTICLE{frankhauser2018probabilistic @article{makoviychuk2021isaac, title={Isaac gym: High performance gpu-based physics simulation for robot learning}, - author={Makoviychuk, Viktor and Wawrzyniak, Lukasz and Guo, Yunrong and Lu, Michelle and Storey, Kier and Macklin, Miles and Hoeller, David and Rudin, Nikita and Allshire, Arthur and Handa, Ankur and others}, + author={Makoviychuk, Viktor and Wawrzyniak, Lukasz and Guo, Yunrong and Lu, Michelle and Storey, Kier and Macklin, Miles and Hoeller, David and Rudin, Nikita and Allshire, Arthur and Handa, Ankur and State, Gavriel}, journal={arXiv preprint arXiv:2108.10470}, year={2021} } @@ -108,21 +108,21 @@ @article{makoviychuk2021isaac @article{handa2022dextreme, title={DeXtreme: Transfer of Agile In-hand Manipulation from Simulation to Reality}, - author={Handa, Ankur and Allshire, Arthur and Makoviychuk, Viktor and Petrenko, Aleksei and Singh, Ritvik and Liu, Jingzhou and Makoviichuk, Denys and Van Wyk, Karl and Zhurkevich, Alexander and Sundaralingam, Balakumar and others}, + author={Handa, Ankur and Allshire, Arthur and Makoviychuk, Viktor and Petrenko, Aleksei and Singh, Ritvik and Liu, Jingzhou and Makoviichuk, Denys and Van Wyk, Karl and Zhurkevich, Alexander and Sundaralingam, Balakumar and Narang, Yashraj and Lafleche, Jean-Francois and Fox, Dieter and State, Gavriel}, journal={arXiv preprint arXiv:2210.13702}, year={2022} } @article{narang2022factory, title={Factory: Fast contact for robotic assembly}, - author={Narang, Yashraj and Storey, Kier and Akinola, Iretiayo and Macklin, Miles and Reist, Philipp and Wawrzyniak, Lukasz and Guo, Yunrong and Moravanszky, Adam and State, Gavriel and Lu, Michelle and others}, + author={Narang, Yashraj and Storey, Kier and Akinola, Iretiayo and Macklin, Miles and Reist, Philipp and Wawrzyniak, Lukasz and Guo, Yunrong and Moravanszky, Adam and State, Gavriel and Lu, Michelle and Handa, Ankur and Fox, Dieter}, journal={arXiv preprint arXiv:2205.03532}, year={2022} } @inproceedings{allshire2022transferring, title={Transferring dexterous manipulation from gpu simulation to a remote real-world trifinger}, - author={Allshire, Arthur and MittaI, Mayank and Lodaya, Varun and Makoviychuk, Viktor and Makoviichuk, Denys and Widmaier, Felix and W{\"u}thrich, Manuel and Bauer, Stefan and Handa, Ankur and Garg, Animesh}, + author={Allshire, Arthur and Mittal, Mayank and Lodaya, Varun and Makoviychuk, Viktor and Makoviichuk, Denys and Widmaier, Felix and W{\"u}thrich, Manuel and Bauer, Stefan and Handa, Ankur and Garg, Animesh}, booktitle={2022 IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS)}, pages={11802--11809}, year={2022}, @@ -139,3 +139,39 @@ @article{mittal2023orbit pages={3740-3747}, doi={10.1109/LRA.2023.3270034} } + +@article{shang2024theia, + title={Theia: Distilling diverse vision foundation models for robot learning}, + author={Shang, Jinghuan and Schmeckpeper, Karl and May, Brandon B and Minniti, Maria Vittoria and Kelestemur, Tarik and Watkins, David and Herlant, Laura}, + journal={arXiv preprint arXiv:2407.20179}, + year={2024} +} + +@inproceedings{he2016deep, + title={Deep residual learning for image recognition}, + author={He, Kaiming and Zhang, Xiangyu and Ren, Shaoqing and Sun, Jian}, + booktitle={Proceedings of the IEEE conference on computer vision and pattern recognition}, + pages={770--778}, + year={2016} +} + +@InProceedings{schwarke2023curiosity, + title = {Curiosity-Driven Learning of Joint Locomotion and Manipulation Tasks}, + author = {Schwarke, Clemens and Klemm, Victor and Boon, Matthijs van der and Bjelonic, Marko and Hutter, Marco}, + booktitle = {Proceedings of The 7th Conference on Robot Learning}, + pages = {2594--2610}, + year = {2023}, + volume = {229}, + series = {Proceedings of Machine Learning Research}, + publisher = {PMLR}, + url = {https://proceedings.mlr.press/v229/schwarke23a.html}, +} + +@InProceedings{mittal2024symmetry, + author={Mittal, Mayank and Rudin, Nikita and Klemm, Victor and Allshire, Arthur and Hutter, Marco}, + booktitle={2024 IEEE International Conference on Robotics and Automation (ICRA)}, + title={Symmetry Considerations for Learning Task Symmetric Robot Policies}, + year={2024}, + pages={7433-7439}, + doi={10.1109/ICRA57147.2024.10611493} +} diff --git a/docs/source/_static/setup/asset_caching.jpg b/docs/source/_static/setup/asset_caching.jpg new file mode 100644 index 0000000..05675fc Binary files /dev/null and b/docs/source/_static/setup/asset_caching.jpg differ diff --git a/docs/source/_static/setup/cloudxr_ar_panel.jpg b/docs/source/_static/setup/cloudxr_ar_panel.jpg new file mode 100644 index 0000000..e92df07 Binary files /dev/null and b/docs/source/_static/setup/cloudxr_ar_panel.jpg differ diff --git a/docs/source/_static/setup/cloudxr_avp_connect_ui.jpg b/docs/source/_static/setup/cloudxr_avp_connect_ui.jpg new file mode 100644 index 0000000..facd34b Binary files /dev/null and b/docs/source/_static/setup/cloudxr_avp_connect_ui.jpg differ diff --git a/docs/source/_static/setup/cloudxr_avp_ik_error.jpg b/docs/source/_static/setup/cloudxr_avp_ik_error.jpg new file mode 100644 index 0000000..3f0d430 Binary files /dev/null and b/docs/source/_static/setup/cloudxr_avp_ik_error.jpg differ diff --git a/docs/source/_static/setup/cloudxr_avp_teleop_ui.jpg b/docs/source/_static/setup/cloudxr_avp_teleop_ui.jpg new file mode 100644 index 0000000..a072ec4 Binary files /dev/null and b/docs/source/_static/setup/cloudxr_avp_teleop_ui.jpg differ diff --git a/docs/source/_static/setup/cloudxr_viewport.jpg b/docs/source/_static/setup/cloudxr_viewport.jpg new file mode 100644 index 0000000..8c41294 Binary files /dev/null and b/docs/source/_static/setup/cloudxr_viewport.jpg differ diff --git a/docs/source/_static/setup/isaac_ants_example.jpg b/docs/source/_static/setup/isaac_ants_example.jpg new file mode 100644 index 0000000..1b428c1 Binary files /dev/null and b/docs/source/_static/setup/isaac_ants_example.jpg differ diff --git a/docs/source/_static/setup/shadow_hands_example.jpg b/docs/source/_static/setup/shadow_hands_example.jpg new file mode 100644 index 0000000..0b910a8 Binary files /dev/null and b/docs/source/_static/setup/shadow_hands_example.jpg differ diff --git a/docs/source/_static/setup/verify_install.jpg b/docs/source/_static/setup/verify_install.jpg new file mode 100644 index 0000000..166840d Binary files /dev/null and b/docs/source/_static/setup/verify_install.jpg differ diff --git a/docs/source/_static/setup/walkthrough_1_1_result.jpg b/docs/source/_static/setup/walkthrough_1_1_result.jpg new file mode 100644 index 0000000..0aa64f4 Binary files /dev/null and b/docs/source/_static/setup/walkthrough_1_1_result.jpg differ diff --git a/docs/source/_static/setup/walkthrough_1_2_arrows.jpg b/docs/source/_static/setup/walkthrough_1_2_arrows.jpg new file mode 100644 index 0000000..81b98ac Binary files /dev/null and b/docs/source/_static/setup/walkthrough_1_2_arrows.jpg differ diff --git a/docs/source/_static/setup/walkthrough_project_setup.svg b/docs/source/_static/setup/walkthrough_project_setup.svg new file mode 100644 index 0000000..f5accff --- /dev/null +++ b/docs/source/_static/setup/walkthrough_project_setup.svg @@ -0,0 +1 @@ +isaac_lab_tutorialscriptssourceLICENSEREADME.mdisaac_lab_tutorialconfigpyproject.tomlsetup.pydocsisaac_lab_tutorialextension.tomltasksdirectisaac_lab_tutorialagents__init__.pyisaac_lab_tutorial_env_cfg.pyisaac_lab_tutorial_env.pyProjectExtensionModulesTask diff --git a/docs/source/_static/setup/walkthrough_sim_stage_scene.svg b/docs/source/_static/setup/walkthrough_sim_stage_scene.svg new file mode 100644 index 0000000..17cd53a --- /dev/null +++ b/docs/source/_static/setup/walkthrough_sim_stage_scene.svg @@ -0,0 +1 @@ +WorldStageSceneSimApp diff --git a/docs/source/_static/setup/walkthrough_stage_context.svg b/docs/source/_static/setup/walkthrough_stage_context.svg new file mode 100644 index 0000000..b98ccb2 --- /dev/null +++ b/docs/source/_static/setup/walkthrough_stage_context.svg @@ -0,0 +1 @@ +OO’/World/Table/Sphere/World/Table/PyramidWorldTablePyramidSphereVisualsVisualsVisuals diff --git a/docs/source/_static/setup/walkthrough_training_vectors.svg b/docs/source/_static/setup/walkthrough_training_vectors.svg new file mode 100644 index 0000000..a285d2c --- /dev/null +++ b/docs/source/_static/setup/walkthrough_training_vectors.svg @@ -0,0 +1 @@ +Wπ‘₯ΰ·œπ‘¦ΰ·œπ‘¦ΰ·œβ€²π‘₯ΰ·œβ€²Bcommandforwardsrobot.data.root_pos_wrobot.data.root_com_vel_w[:,:3]π‘₯ොyawforwardscommand(forwardscommand)𝑧Ƹ diff --git a/docs/source/_static/tasks.jpg b/docs/source/_static/tasks.jpg index 780bc7c..8aa96c1 100644 Binary files a/docs/source/_static/tasks.jpg and b/docs/source/_static/tasks.jpg differ diff --git a/docs/source/_static/tasks/automate/00004.jpg b/docs/source/_static/tasks/automate/00004.jpg new file mode 100644 index 0000000..bf2b3be Binary files /dev/null and b/docs/source/_static/tasks/automate/00004.jpg differ diff --git a/docs/source/_static/tasks/automate/01053_disassembly.jpg b/docs/source/_static/tasks/automate/01053_disassembly.jpg new file mode 100644 index 0000000..a4ff086 Binary files /dev/null and b/docs/source/_static/tasks/automate/01053_disassembly.jpg differ diff --git a/docs/source/_static/tasks/classic/ant.jpg b/docs/source/_static/tasks/classic/ant.jpg index 3497047..d5eadb6 100644 Binary files a/docs/source/_static/tasks/classic/ant.jpg and b/docs/source/_static/tasks/classic/ant.jpg differ diff --git a/docs/source/_static/tasks/classic/cart_double_pendulum.jpg b/docs/source/_static/tasks/classic/cart_double_pendulum.jpg index 5ebc25e..12c3cdb 100644 Binary files a/docs/source/_static/tasks/classic/cart_double_pendulum.jpg and b/docs/source/_static/tasks/classic/cart_double_pendulum.jpg differ diff --git a/docs/source/_static/tasks/classic/cartpole.jpg b/docs/source/_static/tasks/classic/cartpole.jpg index 7ed4299..4eabbb8 100644 Binary files a/docs/source/_static/tasks/classic/cartpole.jpg and b/docs/source/_static/tasks/classic/cartpole.jpg differ diff --git a/docs/source/_static/tasks/classic/humanoid.jpg b/docs/source/_static/tasks/classic/humanoid.jpg index 6c74125..84dc55d 100644 Binary files a/docs/source/_static/tasks/classic/humanoid.jpg and b/docs/source/_static/tasks/classic/humanoid.jpg differ diff --git a/docs/source/_static/tasks/factory/gear_mesh.jpg b/docs/source/_static/tasks/factory/gear_mesh.jpg index ff15d74..af26e58 100644 Binary files a/docs/source/_static/tasks/factory/gear_mesh.jpg and b/docs/source/_static/tasks/factory/gear_mesh.jpg differ diff --git a/docs/source/_static/tasks/factory/nut_thread.jpg b/docs/source/_static/tasks/factory/nut_thread.jpg index 45811da..9a3155e 100644 Binary files a/docs/source/_static/tasks/factory/nut_thread.jpg and b/docs/source/_static/tasks/factory/nut_thread.jpg differ diff --git a/docs/source/_static/tasks/factory/peg_insert.jpg b/docs/source/_static/tasks/factory/peg_insert.jpg index 86ee1ba..7aaa2c1 100644 Binary files a/docs/source/_static/tasks/factory/peg_insert.jpg and b/docs/source/_static/tasks/factory/peg_insert.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/drop.jpg b/docs/source/_static/tasks/future_plans/deformable/drop.jpg index a2c6787..a9b1e15 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/drop.jpg and b/docs/source/_static/tasks/future_plans/deformable/drop.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/flag.jpg b/docs/source/_static/tasks/future_plans/deformable/flag.jpg index 1848fad..006cf96 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/flag.jpg and b/docs/source/_static/tasks/future_plans/deformable/flag.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/fluid_pour.jpg b/docs/source/_static/tasks/future_plans/deformable/fluid_pour.jpg index a27f3e4..03c1a67 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/fluid_pour.jpg and b/docs/source/_static/tasks/future_plans/deformable/fluid_pour.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/fluid_transport.jpg b/docs/source/_static/tasks/future_plans/deformable/fluid_transport.jpg index d19a749..bab737c 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/fluid_transport.jpg and b/docs/source/_static/tasks/future_plans/deformable/fluid_transport.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/pick.jpg b/docs/source/_static/tasks/future_plans/deformable/pick.jpg index 30ce39e..0f749a4 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/pick.jpg and b/docs/source/_static/tasks/future_plans/deformable/pick.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/place.jpg b/docs/source/_static/tasks/future_plans/deformable/place.jpg index 11e3878..4f59e65 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/place.jpg and b/docs/source/_static/tasks/future_plans/deformable/place.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/rope.jpg b/docs/source/_static/tasks/future_plans/deformable/rope.jpg index 6047cf7..93325bf 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/rope.jpg and b/docs/source/_static/tasks/future_plans/deformable/rope.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/shirt-basket.jpg b/docs/source/_static/tasks/future_plans/deformable/shirt-basket.jpg index 535b02c..64b2d72 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/shirt-basket.jpg and b/docs/source/_static/tasks/future_plans/deformable/shirt-basket.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/shirt.jpg b/docs/source/_static/tasks/future_plans/deformable/shirt.jpg index 28f4dd7..578a5d6 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/shirt.jpg and b/docs/source/_static/tasks/future_plans/deformable/shirt.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/stacking.jpg b/docs/source/_static/tasks/future_plans/deformable/stacking.jpg index 45f3464..f9f4343 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/stacking.jpg and b/docs/source/_static/tasks/future_plans/deformable/stacking.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/sweater.jpg b/docs/source/_static/tasks/future_plans/deformable/sweater.jpg index a7bf2c4..c4a61ad 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/sweater.jpg and b/docs/source/_static/tasks/future_plans/deformable/sweater.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/tower_of_hanoi.jpg b/docs/source/_static/tasks/future_plans/deformable/tower_of_hanoi.jpg index 68bae27..c4d1269 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/tower_of_hanoi.jpg and b/docs/source/_static/tasks/future_plans/deformable/tower_of_hanoi.jpg differ diff --git a/docs/source/_static/tasks/future_plans/deformable/vest.jpg b/docs/source/_static/tasks/future_plans/deformable/vest.jpg index 93e91e0..be6c25f 100644 Binary files a/docs/source/_static/tasks/future_plans/deformable/vest.jpg and b/docs/source/_static/tasks/future_plans/deformable/vest.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/beat-the-buzz.jpg b/docs/source/_static/tasks/future_plans/rigid/beat-the-buzz.jpg index 1cb8259..48293cb 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/beat-the-buzz.jpg and b/docs/source/_static/tasks/future_plans/rigid/beat-the-buzz.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/cabinet.jpg b/docs/source/_static/tasks/future_plans/rigid/cabinet.jpg index d903bf0..9594e97 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/cabinet.jpg and b/docs/source/_static/tasks/future_plans/rigid/cabinet.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/hockey.jpg b/docs/source/_static/tasks/future_plans/rigid/hockey.jpg index ec5f227..bb393fe 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/hockey.jpg and b/docs/source/_static/tasks/future_plans/rigid/hockey.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/jenga.jpg b/docs/source/_static/tasks/future_plans/rigid/jenga.jpg index fb408e2..f704abf 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/jenga.jpg and b/docs/source/_static/tasks/future_plans/rigid/jenga.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/lift.jpg b/docs/source/_static/tasks/future_plans/rigid/lift.jpg index 0c29dd2..99dc7fb 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/lift.jpg and b/docs/source/_static/tasks/future_plans/rigid/lift.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/locomotion.jpg b/docs/source/_static/tasks/future_plans/rigid/locomotion.jpg index fb3ef19..3eb8686 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/locomotion.jpg and b/docs/source/_static/tasks/future_plans/rigid/locomotion.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/mobile_cabinet.jpg b/docs/source/_static/tasks/future_plans/rigid/mobile_cabinet.jpg index 27dd648..781a1b2 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/mobile_cabinet.jpg and b/docs/source/_static/tasks/future_plans/rigid/mobile_cabinet.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/mobile_reach.jpg b/docs/source/_static/tasks/future_plans/rigid/mobile_reach.jpg index 40ad56a..577464e 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/mobile_reach.jpg and b/docs/source/_static/tasks/future_plans/rigid/mobile_reach.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/nut-bolt.jpg b/docs/source/_static/tasks/future_plans/rigid/nut-bolt.jpg index 5f1b100..464d781 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/nut-bolt.jpg and b/docs/source/_static/tasks/future_plans/rigid/nut-bolt.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/peg-in-hole.jpg b/docs/source/_static/tasks/future_plans/rigid/peg-in-hole.jpg index 90ac266..8863b7d 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/peg-in-hole.jpg and b/docs/source/_static/tasks/future_plans/rigid/peg-in-hole.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/pyramid.jpg b/docs/source/_static/tasks/future_plans/rigid/pyramid.jpg index 6ea4c4f..38bc4ef 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/pyramid.jpg and b/docs/source/_static/tasks/future_plans/rigid/pyramid.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/reach.jpg b/docs/source/_static/tasks/future_plans/rigid/reach.jpg index 599a7da..ee824e4 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/reach.jpg and b/docs/source/_static/tasks/future_plans/rigid/reach.jpg differ diff --git a/docs/source/_static/tasks/future_plans/rigid/shadow.jpg b/docs/source/_static/tasks/future_plans/rigid/shadow.jpg index 49eaeab..b661ed6 100644 Binary files a/docs/source/_static/tasks/future_plans/rigid/shadow.jpg and b/docs/source/_static/tasks/future_plans/rigid/shadow.jpg differ diff --git a/docs/source/_static/tasks/locomotion/a1_flat.jpg b/docs/source/_static/tasks/locomotion/a1_flat.jpg index 5c6bb0b..c47be98 100644 Binary files a/docs/source/_static/tasks/locomotion/a1_flat.jpg and b/docs/source/_static/tasks/locomotion/a1_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/a1_rough.jpg b/docs/source/_static/tasks/locomotion/a1_rough.jpg index 4e1641e..fdee8d2 100644 Binary files a/docs/source/_static/tasks/locomotion/a1_rough.jpg and b/docs/source/_static/tasks/locomotion/a1_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/agility_digit_flat.jpg b/docs/source/_static/tasks/locomotion/agility_digit_flat.jpg new file mode 100644 index 0000000..f98c15e Binary files /dev/null and b/docs/source/_static/tasks/locomotion/agility_digit_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/agility_digit_loco_manip.jpg b/docs/source/_static/tasks/locomotion/agility_digit_loco_manip.jpg new file mode 100644 index 0000000..5d6abe5 Binary files /dev/null and b/docs/source/_static/tasks/locomotion/agility_digit_loco_manip.jpg differ diff --git a/docs/source/_static/tasks/locomotion/agility_digit_rough.jpg b/docs/source/_static/tasks/locomotion/agility_digit_rough.jpg new file mode 100644 index 0000000..85b9d8f Binary files /dev/null and b/docs/source/_static/tasks/locomotion/agility_digit_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/anymal_b_flat.jpg b/docs/source/_static/tasks/locomotion/anymal_b_flat.jpg index 730a673..f2b6f0b 100644 Binary files a/docs/source/_static/tasks/locomotion/anymal_b_flat.jpg and b/docs/source/_static/tasks/locomotion/anymal_b_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/anymal_b_rough.jpg b/docs/source/_static/tasks/locomotion/anymal_b_rough.jpg index 555b1af..f00c507 100644 Binary files a/docs/source/_static/tasks/locomotion/anymal_b_rough.jpg and b/docs/source/_static/tasks/locomotion/anymal_b_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/anymal_c_flat.jpg b/docs/source/_static/tasks/locomotion/anymal_c_flat.jpg index 0d480f7..295f441 100644 Binary files a/docs/source/_static/tasks/locomotion/anymal_c_flat.jpg and b/docs/source/_static/tasks/locomotion/anymal_c_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/anymal_c_rough.jpg b/docs/source/_static/tasks/locomotion/anymal_c_rough.jpg index 32bacdb..92ebc2f 100644 Binary files a/docs/source/_static/tasks/locomotion/anymal_c_rough.jpg and b/docs/source/_static/tasks/locomotion/anymal_c_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/anymal_d_flat.jpg b/docs/source/_static/tasks/locomotion/anymal_d_flat.jpg index 1d6f595..1e33d34 100644 Binary files a/docs/source/_static/tasks/locomotion/anymal_d_flat.jpg and b/docs/source/_static/tasks/locomotion/anymal_d_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/anymal_d_rough.jpg b/docs/source/_static/tasks/locomotion/anymal_d_rough.jpg index 50bb868..fd57324 100644 Binary files a/docs/source/_static/tasks/locomotion/anymal_d_rough.jpg and b/docs/source/_static/tasks/locomotion/anymal_d_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/g1_flat.jpg b/docs/source/_static/tasks/locomotion/g1_flat.jpg index 624391a..cf9e1b1 100644 Binary files a/docs/source/_static/tasks/locomotion/g1_flat.jpg and b/docs/source/_static/tasks/locomotion/g1_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/g1_rough.jpg b/docs/source/_static/tasks/locomotion/g1_rough.jpg index 71c716a..25ed68d 100644 Binary files a/docs/source/_static/tasks/locomotion/g1_rough.jpg and b/docs/source/_static/tasks/locomotion/g1_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/go1_flat.jpg b/docs/source/_static/tasks/locomotion/go1_flat.jpg index 3ab9caf..4c9319d 100644 Binary files a/docs/source/_static/tasks/locomotion/go1_flat.jpg and b/docs/source/_static/tasks/locomotion/go1_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/go1_rough.jpg b/docs/source/_static/tasks/locomotion/go1_rough.jpg index b5ed7ac..6a4e832 100644 Binary files a/docs/source/_static/tasks/locomotion/go1_rough.jpg and b/docs/source/_static/tasks/locomotion/go1_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/go2_flat.jpg b/docs/source/_static/tasks/locomotion/go2_flat.jpg index 256bd30..0a42c60 100644 Binary files a/docs/source/_static/tasks/locomotion/go2_flat.jpg and b/docs/source/_static/tasks/locomotion/go2_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/go2_rough.jpg b/docs/source/_static/tasks/locomotion/go2_rough.jpg index 7e242af..2baa1ee 100644 Binary files a/docs/source/_static/tasks/locomotion/go2_rough.jpg and b/docs/source/_static/tasks/locomotion/go2_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/h1_flat.jpg b/docs/source/_static/tasks/locomotion/h1_flat.jpg index 2578a9a..2c7cb58 100644 Binary files a/docs/source/_static/tasks/locomotion/h1_flat.jpg and b/docs/source/_static/tasks/locomotion/h1_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/h1_rough.jpg b/docs/source/_static/tasks/locomotion/h1_rough.jpg index f52e964..dc6052f 100644 Binary files a/docs/source/_static/tasks/locomotion/h1_rough.jpg and b/docs/source/_static/tasks/locomotion/h1_rough.jpg differ diff --git a/docs/source/_static/tasks/locomotion/spot_flat.jpg b/docs/source/_static/tasks/locomotion/spot_flat.jpg index 97ff55d..5176156 100644 Binary files a/docs/source/_static/tasks/locomotion/spot_flat.jpg and b/docs/source/_static/tasks/locomotion/spot_flat.jpg differ diff --git a/docs/source/_static/tasks/locomotion/spot_gap.gif b/docs/source/_static/tasks/locomotion/spot_gap.gif deleted file mode 100644 index bfb487a..0000000 --- a/docs/source/_static/tasks/locomotion/spot_gap.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c8a0afef1f24da2f012c181dcfb42e81e76c27b1ff8cd5d805a2d70c5dc21f90 -size 3963991 diff --git a/docs/source/_static/tasks/locomotion/spot_gap.jpg b/docs/source/_static/tasks/locomotion/spot_gap.jpg index 7bcbb29..8a734e3 100644 Binary files a/docs/source/_static/tasks/locomotion/spot_gap.jpg and b/docs/source/_static/tasks/locomotion/spot_gap.jpg differ diff --git a/docs/source/_static/tasks/locomotion/spot_inv_slope.gif b/docs/source/_static/tasks/locomotion/spot_inv_slope.gif deleted file mode 100644 index 8678692..0000000 --- a/docs/source/_static/tasks/locomotion/spot_inv_slope.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce0c03e48d584018d42175f1da89b2af5382642cc94bb93cb85656e3229225b9 -size 6266923 diff --git a/docs/source/_static/tasks/locomotion/spot_obstacle.jpg b/docs/source/_static/tasks/locomotion/spot_obstacle.jpg index 05d56ac..44d8c31 100644 Binary files a/docs/source/_static/tasks/locomotion/spot_obstacle.jpg and b/docs/source/_static/tasks/locomotion/spot_obstacle.jpg differ diff --git a/docs/source/_static/tasks/locomotion/spot_pit.gif b/docs/source/_static/tasks/locomotion/spot_pit.gif deleted file mode 100644 index c1fd803..0000000 --- a/docs/source/_static/tasks/locomotion/spot_pit.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86284bb0f744408b8331e3d51cd7b78ae2996b1073dd93d842a61a711a156759 -size 2986049 diff --git a/docs/source/_static/tasks/locomotion/spot_pit.jpg b/docs/source/_static/tasks/locomotion/spot_pit.jpg index f809c07..2f57ea5 100644 Binary files a/docs/source/_static/tasks/locomotion/spot_pit.jpg and b/docs/source/_static/tasks/locomotion/spot_pit.jpg differ diff --git a/docs/source/_static/tasks/locomotion/spot_slope.jpg b/docs/source/_static/tasks/locomotion/spot_slope.jpg index 07fea59..5db5fb3 100644 Binary files a/docs/source/_static/tasks/locomotion/spot_slope.jpg and b/docs/source/_static/tasks/locomotion/spot_slope.jpg differ diff --git a/docs/source/_static/tasks/locomotion/spot_stepping_stone.gif b/docs/source/_static/tasks/locomotion/spot_stepping_stone.gif deleted file mode 100644 index 04b35ee..0000000 --- a/docs/source/_static/tasks/locomotion/spot_stepping_stone.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6249609204016224ac5c2e8502ef7132982c90b8ad3fbcd3f5137c0cb2a84eaf -size 18962881 diff --git a/docs/source/_static/tasks/locomotion/spot_stepping_stone.jpg b/docs/source/_static/tasks/locomotion/spot_stepping_stone.jpg index 68083ac..591a85a 100644 Binary files a/docs/source/_static/tasks/locomotion/spot_stepping_stone.jpg and b/docs/source/_static/tasks/locomotion/spot_stepping_stone.jpg differ diff --git a/docs/source/_static/tasks/manipulation/agibot_place_mug.jpg b/docs/source/_static/tasks/manipulation/agibot_place_mug.jpg new file mode 100644 index 0000000..73a4217 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/agibot_place_mug.jpg differ diff --git a/docs/source/_static/tasks/manipulation/agibot_place_toy.jpg b/docs/source/_static/tasks/manipulation/agibot_place_toy.jpg new file mode 100644 index 0000000..15f41ba Binary files /dev/null and b/docs/source/_static/tasks/manipulation/agibot_place_toy.jpg differ diff --git a/docs/source/_static/tasks/manipulation/allegro_cube.jpg b/docs/source/_static/tasks/manipulation/allegro_cube.jpg index b75b6ae..7ada7d8 100644 Binary files a/docs/source/_static/tasks/manipulation/allegro_cube.jpg and b/docs/source/_static/tasks/manipulation/allegro_cube.jpg differ diff --git a/docs/source/_static/tasks/manipulation/factory_ext/gear_mesh_ext.jpg b/docs/source/_static/tasks/manipulation/factory_ext/gear_mesh_ext.jpg index 3b44c18..4ffc9ef 100644 Binary files a/docs/source/_static/tasks/manipulation/factory_ext/gear_mesh_ext.jpg and b/docs/source/_static/tasks/manipulation/factory_ext/gear_mesh_ext.jpg differ diff --git a/docs/source/_static/tasks/manipulation/factory_ext/nut_thread_ext.jpg b/docs/source/_static/tasks/manipulation/factory_ext/nut_thread_ext.jpg index 90432bd..1956362 100644 Binary files a/docs/source/_static/tasks/manipulation/factory_ext/nut_thread_ext.jpg and b/docs/source/_static/tasks/manipulation/factory_ext/nut_thread_ext.jpg differ diff --git a/docs/source/_static/tasks/manipulation/factory_ext/peg_insert_ext.jpg b/docs/source/_static/tasks/manipulation/factory_ext/peg_insert_ext.jpg index ed7c24f..d36818f 100644 Binary files a/docs/source/_static/tasks/manipulation/factory_ext/peg_insert_ext.jpg and b/docs/source/_static/tasks/manipulation/factory_ext/peg_insert_ext.jpg differ diff --git a/docs/source/_static/tasks/manipulation/franka_lift.jpg b/docs/source/_static/tasks/manipulation/franka_lift.jpg index 7fa4e61..1784ced 100644 Binary files a/docs/source/_static/tasks/manipulation/franka_lift.jpg and b/docs/source/_static/tasks/manipulation/franka_lift.jpg differ diff --git a/docs/source/_static/tasks/manipulation/franka_open_drawer.jpg b/docs/source/_static/tasks/manipulation/franka_open_drawer.jpg index 94ce61a..42249d5 100644 Binary files a/docs/source/_static/tasks/manipulation/franka_open_drawer.jpg and b/docs/source/_static/tasks/manipulation/franka_open_drawer.jpg differ diff --git a/docs/source/_static/tasks/manipulation/franka_reach.jpg b/docs/source/_static/tasks/manipulation/franka_reach.jpg index 27216fa..19f140c 100644 Binary files a/docs/source/_static/tasks/manipulation/franka_reach.jpg and b/docs/source/_static/tasks/manipulation/franka_reach.jpg differ diff --git a/docs/source/_static/tasks/manipulation/franka_stack.jpg b/docs/source/_static/tasks/manipulation/franka_stack.jpg index 845051d..1da3611 100644 Binary files a/docs/source/_static/tasks/manipulation/franka_stack.jpg and b/docs/source/_static/tasks/manipulation/franka_stack.jpg differ diff --git a/docs/source/_static/tasks/manipulation/g1_pick_place.jpg b/docs/source/_static/tasks/manipulation/g1_pick_place.jpg new file mode 100644 index 0000000..86d2180 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/g1_pick_place.jpg differ diff --git a/docs/source/_static/tasks/manipulation/g1_pick_place_fixed_base.jpg b/docs/source/_static/tasks/manipulation/g1_pick_place_fixed_base.jpg new file mode 100644 index 0000000..ce9d73c Binary files /dev/null and b/docs/source/_static/tasks/manipulation/g1_pick_place_fixed_base.jpg differ diff --git a/docs/source/_static/tasks/manipulation/g1_pick_place_locomanipulation.jpg b/docs/source/_static/tasks/manipulation/g1_pick_place_locomanipulation.jpg new file mode 100644 index 0000000..45d712c Binary files /dev/null and b/docs/source/_static/tasks/manipulation/g1_pick_place_locomanipulation.jpg differ diff --git a/docs/source/_static/tasks/manipulation/galbot_stack_cube.jpg b/docs/source/_static/tasks/manipulation/galbot_stack_cube.jpg new file mode 100644 index 0000000..72b7321 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/galbot_stack_cube.jpg differ diff --git a/docs/source/_static/tasks/manipulation/gr-1_pick_place.jpg b/docs/source/_static/tasks/manipulation/gr-1_pick_place.jpg new file mode 100644 index 0000000..6cbcea1 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/gr-1_pick_place.jpg differ diff --git a/docs/source/_static/tasks/manipulation/gr-1_pick_place_annotation.jpg b/docs/source/_static/tasks/manipulation/gr-1_pick_place_annotation.jpg new file mode 100644 index 0000000..04dd386 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/gr-1_pick_place_annotation.jpg differ diff --git a/docs/source/_static/tasks/manipulation/gr-1_pick_place_waist.jpg b/docs/source/_static/tasks/manipulation/gr-1_pick_place_waist.jpg new file mode 100644 index 0000000..1f99cb7 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/gr-1_pick_place_waist.jpg differ diff --git a/docs/source/_static/tasks/manipulation/kuka_allegro_lift.jpg b/docs/source/_static/tasks/manipulation/kuka_allegro_lift.jpg new file mode 100644 index 0000000..9d19b0e Binary files /dev/null and b/docs/source/_static/tasks/manipulation/kuka_allegro_lift.jpg differ diff --git a/docs/source/_static/tasks/manipulation/kuka_allegro_reorient.jpg b/docs/source/_static/tasks/manipulation/kuka_allegro_reorient.jpg new file mode 100644 index 0000000..384e763 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/kuka_allegro_reorient.jpg differ diff --git a/docs/source/_static/tasks/manipulation/omnireset_drawer_assemble.jpg b/docs/source/_static/tasks/manipulation/omnireset_drawer_assemble.jpg new file mode 100644 index 0000000..4d7bb45 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/omnireset_drawer_assemble.jpg differ diff --git a/docs/source/_static/tasks/manipulation/omnireset_fbleg_screw.jpg b/docs/source/_static/tasks/manipulation/omnireset_fbleg_screw.jpg new file mode 100644 index 0000000..4bb8b88 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/omnireset_fbleg_screw.jpg differ diff --git a/docs/source/_static/tasks/manipulation/omnireset_peg_insert.jpg b/docs/source/_static/tasks/manipulation/omnireset_peg_insert.jpg new file mode 100644 index 0000000..c3413c2 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/omnireset_peg_insert.jpg differ diff --git a/docs/source/_static/tasks/manipulation/shadow_cube.jpg b/docs/source/_static/tasks/manipulation/shadow_cube.jpg index 9123f86..ba30f09 100644 Binary files a/docs/source/_static/tasks/manipulation/shadow_cube.jpg and b/docs/source/_static/tasks/manipulation/shadow_cube.jpg differ diff --git a/docs/source/_static/tasks/manipulation/shadow_hand_over.jpg b/docs/source/_static/tasks/manipulation/shadow_hand_over.jpg index b15ca67..79b71de 100644 Binary files a/docs/source/_static/tasks/manipulation/shadow_hand_over.jpg and b/docs/source/_static/tasks/manipulation/shadow_hand_over.jpg differ diff --git a/docs/source/_static/tasks/manipulation/tycho_track_goal.jpg b/docs/source/_static/tasks/manipulation/tycho_track_goal.jpg index 5736a3d..bcf1acc 100644 Binary files a/docs/source/_static/tasks/manipulation/tycho_track_goal.jpg and b/docs/source/_static/tasks/manipulation/tycho_track_goal.jpg differ diff --git a/docs/source/_static/tasks/manipulation/ur10_reach.jpg b/docs/source/_static/tasks/manipulation/ur10_reach.jpg index faa0e66..f32a241 100644 Binary files a/docs/source/_static/tasks/manipulation/ur10_reach.jpg and b/docs/source/_static/tasks/manipulation/ur10_reach.jpg differ diff --git a/docs/source/_static/tasks/manipulation/ur10_stack_surface_gripper.jpg b/docs/source/_static/tasks/manipulation/ur10_stack_surface_gripper.jpg new file mode 100644 index 0000000..c92b4a3 Binary files /dev/null and b/docs/source/_static/tasks/manipulation/ur10_stack_surface_gripper.jpg differ diff --git a/docs/source/_static/tasks/manipulation/ur10e_reach.jpg b/docs/source/_static/tasks/manipulation/ur10e_reach.jpg new file mode 100644 index 0000000..740b33f Binary files /dev/null and b/docs/source/_static/tasks/manipulation/ur10e_reach.jpg differ diff --git a/docs/source/_static/tasks/manipulation/ur5_track_goal.jpg b/docs/source/_static/tasks/manipulation/ur5_track_goal.jpg index cd6bd2b..dfd344a 100644 Binary files a/docs/source/_static/tasks/manipulation/ur5_track_goal.jpg and b/docs/source/_static/tasks/manipulation/ur5_track_goal.jpg differ diff --git a/docs/source/_static/tasks/manipulation/xarm_leap_track_goal.jpg b/docs/source/_static/tasks/manipulation/xarm_leap_track_goal.jpg index c24354c..f04d54c 100644 Binary files a/docs/source/_static/tasks/manipulation/xarm_leap_track_goal.jpg and b/docs/source/_static/tasks/manipulation/xarm_leap_track_goal.jpg differ diff --git a/docs/source/_static/tasks/navigation/anymal_c_nav.jpg b/docs/source/_static/tasks/navigation/anymal_c_nav.jpg index 9cc7cd5..12766e6 100644 Binary files a/docs/source/_static/tasks/navigation/anymal_c_nav.jpg and b/docs/source/_static/tasks/navigation/anymal_c_nav.jpg differ diff --git a/docs/source/_static/tasks/others/humanoid_amp.jpg b/docs/source/_static/tasks/others/humanoid_amp.jpg index 5503dfd..a30ea26 100644 Binary files a/docs/source/_static/tasks/others/humanoid_amp.jpg and b/docs/source/_static/tasks/others/humanoid_amp.jpg differ diff --git a/docs/source/_static/tasks/others/quadcopter.jpg b/docs/source/_static/tasks/others/quadcopter.jpg index 0bc7dbd..1b62fd8 100644 Binary files a/docs/source/_static/tasks/others/quadcopter.jpg and b/docs/source/_static/tasks/others/quadcopter.jpg differ diff --git a/docs/source/_static/teleop/hand_asset.jpg b/docs/source/_static/teleop/hand_asset.jpg new file mode 100755 index 0000000..240b784 Binary files /dev/null and b/docs/source/_static/teleop/hand_asset.jpg differ diff --git a/docs/source/_static/teleop/teleop_diagram.jpg b/docs/source/_static/teleop/teleop_diagram.jpg new file mode 100755 index 0000000..48d6c29 Binary files /dev/null and b/docs/source/_static/teleop/teleop_diagram.jpg differ diff --git a/docs/source/_static/terrains/height_field/discrete_obstacles_terrain.jpg b/docs/source/_static/terrains/height_field/discrete_obstacles_terrain.jpg deleted file mode 100644 index 43947d4..0000000 --- a/docs/source/_static/terrains/height_field/discrete_obstacles_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4927163189666c35680684d50ce202d895489d4dbc23d9969adc398d769a1232 -size 91428 diff --git a/docs/source/_static/terrains/height_field/discrete_obstacles_terrain_choice.jpg b/docs/source/_static/terrains/height_field/discrete_obstacles_terrain_choice.jpg deleted file mode 100644 index d7d43da..0000000 --- a/docs/source/_static/terrains/height_field/discrete_obstacles_terrain_choice.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e582967fb981a074ac2fcd1bada3f8f542f8fc3eea2fa5b59b86b15cd791450 -size 33975 diff --git a/docs/source/_static/terrains/height_field/discrete_obstacles_terrain_fixed.jpg b/docs/source/_static/terrains/height_field/discrete_obstacles_terrain_fixed.jpg deleted file mode 100644 index 7f54eab..0000000 --- a/docs/source/_static/terrains/height_field/discrete_obstacles_terrain_fixed.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fd9a77be2a92dc714b580b2f90bd72916028acd0ecacb51286de815bfd1d4d67 -size 32427 diff --git a/docs/source/_static/terrains/height_field/inverted_pyramid_sloped_terrain.jpg b/docs/source/_static/terrains/height_field/inverted_pyramid_sloped_terrain.jpg deleted file mode 100644 index a08de8e..0000000 --- a/docs/source/_static/terrains/height_field/inverted_pyramid_sloped_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c07615709339b8c01610ec3bd1eb0f53338df0bb39442d2b16443d204b8fc1a -size 148994 diff --git a/docs/source/_static/terrains/height_field/inverted_pyramid_stairs_terrain.jpg b/docs/source/_static/terrains/height_field/inverted_pyramid_stairs_terrain.jpg deleted file mode 100644 index cd86e92..0000000 --- a/docs/source/_static/terrains/height_field/inverted_pyramid_stairs_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:826e96d56ebf28b27bd9cde1822fc79a98c5650d1e3a88d44ecc86570c967d38 -size 47349 diff --git a/docs/source/_static/terrains/height_field/pyramid_sloped_terrain.jpg b/docs/source/_static/terrains/height_field/pyramid_sloped_terrain.jpg deleted file mode 100644 index a8feb34..0000000 --- a/docs/source/_static/terrains/height_field/pyramid_sloped_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:080d9fc8a3d54e14d5f370c982dd913b98b5b93c035314b6333f0bae4583556e -size 150200 diff --git a/docs/source/_static/terrains/height_field/pyramid_stairs_terrain.jpg b/docs/source/_static/terrains/height_field/pyramid_stairs_terrain.jpg deleted file mode 100644 index a9f53ee..0000000 --- a/docs/source/_static/terrains/height_field/pyramid_stairs_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c4a038949988eab9887fdd078421acc743c245d9b4d78a2934f5c431b3998ff -size 29207 diff --git a/docs/source/_static/terrains/height_field/random_uniform_terrain.jpg b/docs/source/_static/terrains/height_field/random_uniform_terrain.jpg deleted file mode 100644 index 47c194a..0000000 --- a/docs/source/_static/terrains/height_field/random_uniform_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8023cf72f1addf993ccf4037c061ac3fbe8c2938e52870c8d7cec3ca12c5fd15 -size 333513 diff --git a/docs/source/_static/terrains/height_field/stepping_stones_terrain.jpg b/docs/source/_static/terrains/height_field/stepping_stones_terrain.jpg deleted file mode 100644 index b276619..0000000 --- a/docs/source/_static/terrains/height_field/stepping_stones_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8ec37e7717d8c92e6e39988edfa2a934e7aace62c72a92ce794bff1f71e1baa8 -size 248578 diff --git a/docs/source/_static/terrains/height_field/wave_terrain.jpg b/docs/source/_static/terrains/height_field/wave_terrain.jpg deleted file mode 100644 index 841e320..0000000 --- a/docs/source/_static/terrains/height_field/wave_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:24656af96f83e10872cc6112b57f40e47e7e6d4f4f983bc728a74c922dac9139 -size 282380 diff --git a/docs/source/_static/terrains/trimesh/box_terrain.jpg b/docs/source/_static/terrains/trimesh/box_terrain.jpg deleted file mode 100644 index c4af3eb..0000000 --- a/docs/source/_static/terrains/trimesh/box_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f233bebb033a074a71720b853e64bbc863c57ded47b69d18064059cf4304f06f -size 2956 diff --git a/docs/source/_static/terrains/trimesh/box_terrain_with_two_boxes.jpg b/docs/source/_static/terrains/trimesh/box_terrain_with_two_boxes.jpg deleted file mode 100644 index 9d06bed..0000000 --- a/docs/source/_static/terrains/trimesh/box_terrain_with_two_boxes.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c517ab4fd58e280fbc1b97c4b85c57960e8af93cbe388c6580321a5b5ff1c362 -size 2993 diff --git a/docs/source/_static/terrains/trimesh/flat_terrain.jpg b/docs/source/_static/terrains/trimesh/flat_terrain.jpg deleted file mode 100644 index 26776e3..0000000 --- a/docs/source/_static/terrains/trimesh/flat_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d0907c5a2cd6b3ee287931578b7bdba1123d968abacbd6a7f13a796a22c4e1a -size 2852 diff --git a/docs/source/_static/terrains/trimesh/floating_ring_terrain.jpg b/docs/source/_static/terrains/trimesh/floating_ring_terrain.jpg deleted file mode 100644 index 41d53ce..0000000 --- a/docs/source/_static/terrains/trimesh/floating_ring_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11f4f382c9ebfa3759af8471151e2840126874f26a0b3a00bad804012696a773 -size 2995 diff --git a/docs/source/_static/terrains/trimesh/gap_terrain.jpg b/docs/source/_static/terrains/trimesh/gap_terrain.jpg deleted file mode 100644 index 8810fca..0000000 --- a/docs/source/_static/terrains/trimesh/gap_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7aeb68558d35d186f6be3dc845d1b775a8f233fbbcc86c6a130a0ae204cac71d -size 3708 diff --git a/docs/source/_static/terrains/trimesh/inverted_pyramid_stairs_terrain.jpg b/docs/source/_static/terrains/trimesh/inverted_pyramid_stairs_terrain.jpg deleted file mode 100644 index 326ef95..0000000 --- a/docs/source/_static/terrains/trimesh/inverted_pyramid_stairs_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:edced4bb3f37679330ac861b710546989048bd549f5ba6aa4e23319733e89e3c -size 5796 diff --git a/docs/source/_static/terrains/trimesh/inverted_pyramid_stairs_terrain_with_holes.jpg b/docs/source/_static/terrains/trimesh/inverted_pyramid_stairs_terrain_with_holes.jpg deleted file mode 100644 index 96e3b0c..0000000 --- a/docs/source/_static/terrains/trimesh/inverted_pyramid_stairs_terrain_with_holes.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47cfdc0d011e4353e3e15c11b011444fd46b361b6445d121b12ee6696de22af8 -size 6353 diff --git a/docs/source/_static/terrains/trimesh/pit_terrain.jpg b/docs/source/_static/terrains/trimesh/pit_terrain.jpg deleted file mode 100644 index 51aabf2..0000000 --- a/docs/source/_static/terrains/trimesh/pit_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc004b96d8db674643f44e93ff3aae8af4b813c9475cab7243a15ac8dfa67c4f -size 3218 diff --git a/docs/source/_static/terrains/trimesh/pit_terrain_with_two_levels.jpg b/docs/source/_static/terrains/trimesh/pit_terrain_with_two_levels.jpg deleted file mode 100644 index eb5a862..0000000 --- a/docs/source/_static/terrains/trimesh/pit_terrain_with_two_levels.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0c89bb5b34e29e6e402848e58ce79da1fb453d58eb2983be1518daf4a51966da -size 4370 diff --git a/docs/source/_static/terrains/trimesh/pyramid_stairs_terrain.jpg b/docs/source/_static/terrains/trimesh/pyramid_stairs_terrain.jpg deleted file mode 100644 index be7fa69..0000000 --- a/docs/source/_static/terrains/trimesh/pyramid_stairs_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:12975e7119588cb1c0c63e77fed803552e1e5a4008af58198d01e80e89314691 -size 3595 diff --git a/docs/source/_static/terrains/trimesh/pyramid_stairs_terrain_with_holes.jpg b/docs/source/_static/terrains/trimesh/pyramid_stairs_terrain_with_holes.jpg deleted file mode 100644 index 836dc6e..0000000 --- a/docs/source/_static/terrains/trimesh/pyramid_stairs_terrain_with_holes.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e63145b71405a1c57bf153f0365b82a6f35f5894fd22a9676b4c030da723b440 -size 4926 diff --git a/docs/source/_static/terrains/trimesh/rails_terrain.jpg b/docs/source/_static/terrains/trimesh/rails_terrain.jpg deleted file mode 100644 index cc112e9..0000000 --- a/docs/source/_static/terrains/trimesh/rails_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:397e2a0f459c0923cc3f07e954a9835cf00a659f5ecc312465ffa0930fdee1fd -size 3326 diff --git a/docs/source/_static/terrains/trimesh/random_grid_terrain.jpg b/docs/source/_static/terrains/trimesh/random_grid_terrain.jpg deleted file mode 100644 index dba9100..0000000 --- a/docs/source/_static/terrains/trimesh/random_grid_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cee6ed4e4e2e2191c1dd419c1c59d365f772bb1ff045b45771fe848dfd396ce7 -size 6378 diff --git a/docs/source/_static/terrains/trimesh/random_grid_terrain_with_holes.jpg b/docs/source/_static/terrains/trimesh/random_grid_terrain_with_holes.jpg deleted file mode 100644 index 3cd4f54..0000000 --- a/docs/source/_static/terrains/trimesh/random_grid_terrain_with_holes.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1be4b07068e25e7b402999ab4eb181c3115c60dc95d9f7f7a3f6296e8540167a -size 6093 diff --git a/docs/source/_static/terrains/trimesh/repeated_objects_box_terrain.jpg b/docs/source/_static/terrains/trimesh/repeated_objects_box_terrain.jpg deleted file mode 100644 index 4af1d1f..0000000 --- a/docs/source/_static/terrains/trimesh/repeated_objects_box_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bf00b570b6cb5cf748c764144d4818ea3878abb48860a4202f03e112f653d46c -size 60154 diff --git a/docs/source/_static/terrains/trimesh/repeated_objects_cylinder_terrain.jpg b/docs/source/_static/terrains/trimesh/repeated_objects_cylinder_terrain.jpg deleted file mode 100644 index 4ebbbe0..0000000 --- a/docs/source/_static/terrains/trimesh/repeated_objects_cylinder_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef6f85e8fe9f494d9743bdb23846b4e71c33c8998e7b91d0b5429b610cb4f6de -size 84510 diff --git a/docs/source/_static/terrains/trimesh/repeated_objects_pyramid_terrain.jpg b/docs/source/_static/terrains/trimesh/repeated_objects_pyramid_terrain.jpg deleted file mode 100644 index 0105dc3..0000000 --- a/docs/source/_static/terrains/trimesh/repeated_objects_pyramid_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2f000868c88452b9596ffa8f5f78a04e267ea94bb97f49ce71564efc15972d0a -size 62332 diff --git a/docs/source/_static/terrains/trimesh/star_terrain.jpg b/docs/source/_static/terrains/trimesh/star_terrain.jpg deleted file mode 100644 index 281916c..0000000 --- a/docs/source/_static/terrains/trimesh/star_terrain.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22c69f97496f7c81cb3adce0725f6f80e96c6ab78cbcd3b97929e87b4397ed24 -size 9751 diff --git a/docs/source/_static/uwlab.jpg b/docs/source/_static/uwlab.jpg new file mode 100644 index 0000000..75b3fc8 Binary files /dev/null and b/docs/source/_static/uwlab.jpg differ diff --git a/docs/source/_static/uwlab.png b/docs/source/_static/uwlab.png deleted file mode 100644 index 1d8900f..0000000 --- a/docs/source/_static/uwlab.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c6345fb85374e6f9be83e839299997b75aad26b07d442605bbe6a84a653f998e -size 209769 diff --git a/docs/source/_static/vscode_tasks.png b/docs/source/_static/vscode_tasks.png index b5b0d5c..41d5cdb 100644 Binary files a/docs/source/_static/vscode_tasks.png and b/docs/source/_static/vscode_tasks.png differ diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst deleted file mode 100644 index da2d41a..0000000 --- a/docs/source/api/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -API Reference -============= - -This page gives an overview of all the modules and classes in the Isaac Lab extensions. - -uwlab extension ------------------------- - -The following modules are available in the ``uwlab`` extension: - -.. currentmodule:: uwlab - -.. autosummary:: - :toctree: lab - - controllers - devices diff --git a/docs/source/api/lab/uwlab.controllers.rst b/docs/source/api/lab/uwlab.controllers.rst deleted file mode 100644 index ee54df4..0000000 --- a/docs/source/api/lab/uwlab.controllers.rst +++ /dev/null @@ -1,25 +0,0 @@ -ο»Ώuwlab.controllers -==================== - -.. automodule:: uwlab.controllers - - .. rubric:: Classes - - .. autosummary:: - - MultiConstraintDifferentialIKController - MultiConstraintDifferentialIKControllerCfg - -Differential Inverse Kinematics With Multiple Bodies ------------------------------------------------------ - -.. autoclass:: MultiConstraintDifferentialIKController - :members: - :inherited-members: - :show-inheritance: - -.. autoclass:: MultiConstraintDifferentialIKControllerCfg - :members: - :inherited-members: - :show-inheritance: - :exclude-members: __init__, __new__, class_type diff --git a/docs/source/api/lab/uwlab.devices.rst b/docs/source/api/lab/uwlab.devices.rst deleted file mode 100644 index 20f58a4..0000000 --- a/docs/source/api/lab/uwlab.devices.rst +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώuwlab.devices -=============== - -.. automodule:: uwlab.devices - - .. rubric:: Classes - - .. autosummary:: - - Se3Keyboard - RealsenseT265 - RokokoGlove - - -Keyboard --------- - -.. autoclass:: Se3Keyboard - :members: - :inherited-members: - :show-inheritance: - -Real Sense ----------- -.. autoclass:: RealsenseT265 - :members: - :inherited-members: - :show-inheritance: - -Rokoko Gloves -------------- - -.. autoclass:: RokokoGlove - :members: - :inherited-members: - :show-inheritance: diff --git a/docs/source/deployment/cluster.rst b/docs/source/deployment/cluster.rst new file mode 100644 index 0000000..7473b27 --- /dev/null +++ b/docs/source/deployment/cluster.rst @@ -0,0 +1,211 @@ +.. _deployment-cluster: + + +Cluster Guide +============= + +Clusters are a great way to speed up training and evaluation of learning algorithms. +While the UW Lab Docker image can be used to run jobs on a cluster, many clusters only +support singularity images. This is because `singularity`_ is designed for +ease-of-use on shared multi-user systems and high performance computing (HPC) environments. +It does not require root privileges to run containers and can be used to run user-defined +containers. + +Singularity is compatible with all Docker images. In this section, we describe how to +convert the UW Lab Docker image into a singularity image and use it to submit jobs to a cluster. + +.. attention:: + + Cluster setup varies across different institutions. The following instructions have been + tested on the `ETH Zurich Euler`_ cluster (which uses the SLURM workload manager), and the + IIT Genoa Franklin cluster (which uses PBS workload manager). + + The instructions may need to be adapted for other clusters. If you have successfully + adapted the instructions for another cluster, please consider contributing to the + documentation. + + +Setup Instructions +------------------ + +In order to export the Docker Image to a singularity image, `apptainer`_ is required. +A detailed overview of the installation procedure for ``apptainer`` can be found in its +`documentation`_. For convenience, we summarize the steps here for a local installation: + +.. code:: bash + + sudo apt update + sudo apt install -y software-properties-common + sudo add-apt-repository -y ppa:apptainer/ppa + sudo apt update + sudo apt install -y apptainer + +For simplicity, we recommend that an SSH connection is set up between the local +development machine and the cluster. Such a connection will simplify the file transfer and prevent +the user cluster password from being requested multiple times. + +.. attention:: + The workflow has been tested with: + + - ``apptainer version 1.2.5-1.el7`` and ``docker version 24.0.7`` + - ``apptainer version 1.3.4`` and ``docker version 27.3.1`` + + In the case of issues, please try to switch to those versions. + + +Configuring the cluster parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, you need to configure the cluster-specific parameters in ``docker/cluster/.env.cluster`` file. +The following describes the parameters that need to be configured: + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Parameter + - Description + * - CLUSTER_JOB_SCHEDULER + - The job scheduler/workload manager used by your cluster. Currently, we support 'SLURM' and + 'PBS' workload managers. + * - CLUSTER_ISAAC_SIM_CACHE_DIR + - The directory on the cluster where the Isaac Sim cache is stored. This directory + has to end on ``docker-isaac-sim``. It will be copied to the compute node + and mounted into the singularity container. This should increase the speed of starting + the simulation. + * - CLUSTER_UWLAB_DIR + - The directory on the cluster where the UW Lab logs are stored. This directory has to + end on ``uwlab``. It will be copied to the compute node and mounted into + the singularity container. When a job is submitted, the latest local changes will + be copied to the cluster to a new directory in the format ``${CLUSTER_UWLAB_DIR}_${datetime}`` + with the date and time of the job submission. This allows to run multiple jobs with different code versions at + the same time. + * - CLUSTER_LOGIN + - The login to the cluster. Typically, this is the user and cluster names, + e.g., ``your_user@euler.ethz.ch``. + * - CLUSTER_SIF_PATH + - The path on the cluster where the singularity image will be stored. The image will be + copied to the compute node but not uploaded again to the cluster when a job is submitted. + * - REMOVE_CODE_COPY_AFTER_JOB + - Whether the copied code should be removed after the job is finished or not. The logs from the job will not be deleted + as these are saved under the permanent ``CLUSTER_UWLAB_DIR``. This feature is useful + to save disk space on the cluster. If set to ``true``, the code copy will be removed. + * - CLUSTER_PYTHON_EXECUTABLE + - The path within UW Lab to the Python executable that should be executed in the submitted job. + +When a ``job`` is submitted, it will also use variables defined in ``docker/.env.base``, though these +should be correct by default. + +Exporting to singularity image +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Next, we need to export the Docker image to a singularity image and upload +it to the cluster. This step is only required once when the first job is submitted +or when the Docker image is updated. For instance, due to an upgrade of the Isaac Sim +version, or additional requirements for your project. + +To export to a singularity image, execute the following command: + +.. code:: bash + + ./docker/cluster/cluster_interface.sh push [profile] + +This command will create a singularity image under ``docker/exports`` directory and +upload it to the defined location on the cluster. It requires that you have previously +built the image with the ``container.py`` interface. Be aware that creating the singularity +image can take a while. +``[profile]`` is an optional argument that specifies the container profile to be used. If no profile is +specified, the default profile ``base`` will be used. + +.. note:: + By default, the singularity image is created without root access by providing the ``--fakeroot`` flag to + the ``apptainer build`` command. In case the image creation fails, you can try to create it with root + access by removing the flag in ``docker/cluster/cluster_interface.sh``. + + +Defining the job parameters +--------------------------- + +The job parameters need to be defined based on the job scheduler used by your cluster. +You only need to update the appropriate script for the scheduler available to you. + +- For SLURM, update the parameters in ``docker/cluster/submit_job_slurm.sh``. +- For PBS, update the parameters in ``docker/cluster/submit_job_pbs.sh``. + +For SLURM +~~~~~~~~~ + +The job parameters are defined inside the ``docker/cluster/submit_job_slurm.sh``. +A typical SLURM operation requires specifying the number of CPUs and GPUs, the memory, and +the time limit. For more information, please check the `SLURM documentation`_. + +The default configuration is as follows: + +.. literalinclude:: ../../../docker/cluster/submit_job_slurm.sh + :language: bash + :lines: 12-19 + :linenos: + :lineno-start: 12 + +An essential requirement for the cluster is that the compute node has access to the internet at all times. +This is required to load assets from the Nucleus server. For some cluster architectures, extra modules +must be loaded to allow internet access. + +For instance, on ETH Zurich Euler cluster, the ``eth_proxy`` module needs to be loaded. This can be done +by adding the following line to the ``submit_job_slurm.sh`` script: + +.. literalinclude:: ../../../docker/cluster/submit_job_slurm.sh + :language: bash + :lines: 3-5 + :linenos: + :lineno-start: 3 + +For PBS +~~~~~~~ + +The job parameters are defined inside the ``docker/cluster/submit_job_pbs.sh``. +A typical PBS operation requires specifying the number of CPUs and GPUs, and the time limit. For more +information, please check the `PBS Official Site`_. + +The default configuration is as follows: + +.. literalinclude:: ../../../docker/cluster/submit_job_pbs.sh + :language: bash + :lines: 11-17 + :linenos: + :lineno-start: 11 + + +Submitting a job +---------------- + +To submit a job on the cluster, the following command can be used: + +.. code:: bash + + ./docker/cluster/cluster_interface.sh job [profile] "argument1" "argument2" ... + +This command will copy the latest changes in your code to the cluster and submit a job. Please ensure that +your Python executable's output is stored under ``uwlab/logs`` as this directory is synced between the compute +node and ``CLUSTER_UWLAB_DIR``. + +``[profile]`` is an optional argument that specifies which singularity image corresponding to the container profile +will be used. If no profile is specified, the default profile ``base`` will be used. The profile has be defined +directlty after the ``job`` command. All other arguments are passed to the Python executable. If no profile is +defined, all arguments are passed to the Python executable. + +The training arguments are passed to the Python executable. As an example, the standard +ANYmal rough terrain locomotion training can be executed with the following command: + +.. code:: bash + + ./docker/cluster/cluster_interface.sh job --task Isaac-Velocity-Rough-Anymal-C-v0 --headless --video --enable_cameras + +The above will, in addition, also render videos of the training progress and store them under ``uwlab/logs`` directory. + +.. _Singularity: https://docs.sylabs.io/guides/2.6/user-guide/index.html +.. _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 +.. _SLURM documentation: https://www.slurm.schedmd.com/sbatch.html diff --git a/docs/source/deployment/docker.rst b/docs/source/deployment/docker.rst new file mode 100644 index 0000000..832df70 --- /dev/null +++ b/docs/source/deployment/docker.rst @@ -0,0 +1,368 @@ +.. _deployment-docker: + + +Docker Guide +============ + +.. caution:: + + Due to the dependency on Isaac Sim docker image, by running this container you are implicitly + agreeing to the `NVIDIA Software License Agreement`_. If you do not agree to the EULA, do not run this container. + +Setup Instructions +------------------ + +.. note:: + + The following steps are taken from the Isaac Sim documentation on `container installation`_. + They have been added here for the sake of completeness. + + +Docker and Docker Compose +~~~~~~~~~~~~~~~~~~~~~~~~~ + +We have tested the container using Docker Engine version 26.0.0 and Docker Compose version 2.25.0 +We recommend using these versions or newer. + +* To install Docker, please follow the instructions for your operating system on the `Docker website`_. +* To install Docker Compose, please follow the instructions for your operating system on the `docker compose`_ page. +* Follow the post-installation steps for Docker on the `post-installation steps`_ page. These steps allow you to run + Docker without using ``sudo``. +* To build and run GPU-accelerated containers, you also need install the `NVIDIA Container Toolkit`_. + Please follow the instructions on the `Container Toolkit website`_ for installation steps. + +.. note:: + + Due to limitations with `snap `_, please make sure + the UW Lab directory is placed under the ``/home`` directory tree when using docker. + + +Directory Organization +---------------------- + +The root of the UW Lab repository contains the ``docker`` directory that has various files and scripts +needed to run UW Lab inside a Docker container. A subset of these are summarized below: + +* **Dockerfile.base**: Defines the base UW Lab image by overlaying its dependencies onto the Isaac Sim Docker image. + Dockerfiles which end with something else, (i.e. ``Dockerfile.ros2``) build an `image extension <#uw-lab-image-extensions>`_. +* **docker-compose.yaml**: Creates mounts to allow direct editing of UW 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. +* **.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 <#uw-lab-image-extensions>`_. +* **docker-compose.cloudxr-runtime.patch.yaml**: A patch file that is applied to enable CloudXR Runtime support for + streaming to compatible XR devices. It defines services and volumes for CloudXR Runtime and the base. +* **.env.cloudxr-runtime**: Environment variables for the CloudXR Runtime support. +* **container.py**: A utility script that interfaces with tools in ``utils`` to configure and build the image, + and run and interact with the container. + +Running the Container +--------------------- + +.. note:: + + The docker container copies all the files from the repository into the container at the + location ``/workspace/uwlab`` at build time. This means that any changes made to the files in the container would not + normally be reflected in the repository after the image has been built, i.e. after ``./container.py start`` is run. + + For a faster development cycle, we mount the following directories in the UW Lab repository into the container + so that you can edit their files from the host machine: + + * **UWLab/source**: This is the directory that contains the UW Lab source code. + * **UWLab/docs**: This is the directory that contains the source code for UW Lab documentation. This is overlaid except + for the ``_build`` subdirectory where build artifacts are stored. + + +The script ``container.py`` parallels basic ``docker compose`` commands. Each can accept an `image extension argument <#uw-lab-image-extensions>`_, +or else they will default to the ``base`` image extension. These commands are: + +* **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 UW Lab container, and which can be exited + without bringing down the container. +* **config**: This outputs the compose.yaml which would be result from the inputs given to ``container.py start``. This command is useful + for debugging a compose configuration. +* **copy**: This copies the ``logs``, ``data_storage`` and ``docs/_build`` artifacts, from the ``uw-lab-logs``, ``uw-lab-data`` and ``uw-lab-docs`` + volumes respectively, to the ``docker/artifacts`` directory. These artifacts persist between docker container instances and are shared between image extensions. +* **stop**: This brings down the container and removes it. + +The following shows how to launch the container in a detached state and enter it: + +.. code:: bash + + # Launch the container in detached mode + # We don't pass an image extension arg, so it defaults to 'base' + ./docker/container.py start + + # If we want to add .env or .yaml files to customize our compose config, + # we can simply specify them in the same manner as the compose cli + # ./docker/container.py start --file my-compose.yaml --env-file .env.my-vars + + # Enter the container + # We pass 'base' explicitly, but if we hadn't it would default to 'base' + ./docker/container.py enter base + +To copy files from the base container to the host machine, you can use the following command: + +.. code:: bash + + # Copy the file /workspace/uwlab/logs to the current directory + docker cp uw-lab-base:/workspace/uwlab/logs . + +The script ``container.py`` provides a wrapper around this command to copy the ``logs`` , ``data_storage`` and ``docs/_build`` +directories to the ``docker/artifacts`` directory. This is useful for copying the logs, data and documentation: + +.. code:: bash + + # stop the container + ./docker/container.py stop + + +CloudXR Runtime Support +~~~~~~~~~~~~~~~~~~~~~~~ + +To enable CloudXR Runtime for streaming to compatible XR devices, you need to apply the patch file +``docker-compose.cloudxr-runtime.patch.yaml`` to run CloudXR Runtime container. The patch file defines services and +volumes for CloudXR Runtime and base. The environment variables required for CloudXR Runtime are specified in the +``.env.cloudxr-runtime`` file. To start or stop the CloudXR runtime container with base, use the following command: + +.. code:: bash + + # Start CloudXR Runtime container with base. + ./docker/container.py start --files docker-compose.cloudxr-runtime.patch.yaml --env-file .env.cloudxr-runtime + + # Stop CloudXR Runtime container and base. + ./docker/container.py stop --files docker-compose.cloudxr-runtime.patch.yaml --env-file .env.cloudxr-runtime + + +X11 forwarding +~~~~~~~~~~~~~~ + +The container supports X11 forwarding, which allows the user to run GUI applications from the container +and display them on the host machine. + +The first time a container is started with ``./docker/container.py start``, the script prompts +the user whether to activate X11 forwarding. This will create a file at ``docker/.container.cfg`` +to store the user's choice for future runs. + +If you want to change the choice, you can set the parameter ``X11_FORWARDING_ENABLED`` to '0' or '1' +in the ``docker/.container.cfg`` file to disable or enable X11 forwarding, respectively. After that, you need to +re-build the container by running ``./docker/container.py start``. The rebuilding process ensures that the changes +are applied to the container. Otherwise, the changes will not take effect. + +After the container is started, you can enter the container and run GUI applications from it with X11 forwarding enabled. +The display will be forwarded to the host machine. + + +Python Interpreter +~~~~~~~~~~~~~~~~~~ + +The container uses the Python interpreter provided by Isaac Sim. This interpreter is located at +``/isaac-sim/python.sh``. We set aliases inside the container to make it easier to run the Python +interpreter. You can use the following commands to run the Python interpreter: + +.. code:: bash + + # Run the Python interpreter -> points to /isaac-sim/python.sh + python + + +Understanding the mounted volumes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``docker-compose.yaml`` file creates several named volumes that are mounted to the container. +These are summarized below: + +.. list-table:: + :header-rows: 1 + :widths: 23 45 32 + + * - Volume Name + - Description + - Container Path + * - isaac-cache-kit + - Stores cached Kit resources + - /isaac-sim/kit/cache + * - isaac-cache-ov + - Stores cached OV resources + - /root/.cache/ov + * - isaac-cache-pip + - Stores cached pip resources + - /root/.cache/pip + * - isaac-cache-gl + - Stores cached GLCache resources + - /root/.cache/nvidia/GLCache + * - isaac-cache-compute + - Stores cached compute resources + - /root/.nv/ComputeCache + * - isaac-logs + - Stores logs generated by Omniverse + - /root/.nvidia-omniverse/logs + * - isaac-carb-logs + - Stores logs generated by carb + - /isaac-sim/kit/logs/Kit/Isaac-Sim + * - isaac-data + - Stores data generated by Omniverse + - /root/.local/share/ov/data + * - isaac-docs + - Stores documents generated by Omniverse + - /root/Documents + * - uw-lab-docs + - Stores documentation of UW Lab when built inside the container + - /workspace/uwlab/docs/_build + * - uw-lab-logs + - Stores logs generated by UW Lab workflows when run inside the container + - /workspace/uwlab/logs + * - uw-lab-data + - Stores whatever data users may want to preserve between container runs + - /workspace/uwlab/data_storage + +To view the contents of these volumes, you can use the following command: + +.. code:: bash + + # list all volumes + docker volume ls + # inspect a specific volume, e.g. isaac-cache-kit + docker volume inspect isaac-cache-kit + + + +UW Lab Image Extensions +-------------------------- + +The produced image depends on the arguments passed to ``container.py start`` and ``container.py stop``. These +commands accept an image extension parameter as an additional argument. If no argument is passed, then this +parameter defaults to ``base``. Currently, the only valid values are (``base``, ``ros2``). +Only one image extension can be passed at a time. The produced image and container will be named +``uw-lab-${profile}``, where ``${profile}`` is the image extension name. + +``suffix`` is an optional string argument to ``container.py`` that specifies a docker image and +container name suffix, which can be useful for development purposes. By default ``${suffix}`` is the empty string. +If ``${suffix}`` is a nonempty string, then the produced docker image and container will be named +``uw-lab-${profile}-${suffix}``, where a hyphen is inserted between ``${profile}`` and ``${suffix}`` in +the name. ``suffix`` should not be used with cluster deployments. + +.. code:: bash + + # start base by default, named uw-lab-base + ./docker/container.py start + # stop base explicitly, named uw-lab-base + ./docker/container.py stop base + # start ros2 container named uw-lab-ros2 + ./docker/container.py start ros2 + # stop ros2 container named uw-lab-ros2 + ./docker/container.py stop ros2 + + # start base container named uw-lab-base-custom + ./docker/container.py start base --suffix custom + # stop base container named uw-lab-base-custom + ./docker/container.py stop base --suffix custom + # start ros2 container named uw-lab-ros2-custom + ./docker/container.py start ros2 --suffix custom + # stop ros2 container named uw-lab-ros2-custom + ./docker/container.py stop ros2 --suffix custom + +The passed image extension argument will build the image defined in ``Dockerfile.${image_extension}``, +with the corresponding `profile`_ in the ``docker-compose.yaml`` and the envars from ``.env.${image_extension}`` +in addition to the ``.env.base``, if any. + +ROS2 Image Extension +~~~~~~~~~~~~~~~~~~~~ + +In ``Dockerfile.ros2``, the container installs ROS2 Humble via an `apt package`_, and it is sourced in the ``.bashrc``. +The exact version is specified by the variable ``ROS_APT_PACKAGE`` in the ``.env.ros2`` file, +defaulting to ``ros-base``. Other relevant ROS2 variables are also specified in the ``.env.ros2`` file, +including variables defining the `various middleware`_ options. + +The container defaults to ``FastRTPS``, but ``CylconeDDS`` is also supported. Each of these middlewares can be +`tuned`_ using their corresponding ``.xml`` files under ``docker/.ros``. + + +.. dropdown:: Parameters for ROS2 Image Extension + :icon: code + + .. literalinclude:: ../../../docker/.env.ros2 + :language: bash + + +Running Pre-Built UW Lab Container +------------------------------------- + +In UW Lab 2.0 release, we introduced a minimal pre-built container that contains a very minimal set +of Isaac Sim and Omniverse dependencies, along with UW Lab 2.0 pre-built into the container. +This container allows users to pull the container directly from NGC without requiring a local build of +the docker image. The UW Lab source code will be available in this container under ``/workspace/UWLab``. + +This container is designed for running **headless** only and does not allow for X11 forwarding or running +with the GUI. Please only use this container for headless training. For other use cases, we recommend +following the above steps to build your own UW Lab docker image. + +.. note:: + + Currently, we only provide docker images with every major release of UW Lab. + For example, we provide the docker image for release 2.0.0 and 2.1.0, but not 2.0.2. + In the future, we will provide docker images for every minor release of UW Lab. + +To pull the minimal UW Lab container, run: + +.. code:: bash + + docker pull nvcr.io/nvidia/uw-lab:2.3.0 + +To run the UW Lab container with an interactive bash session, run: + +.. code:: bash + + docker run --name uw-lab --entrypoint bash -it --gpus all -e "ACCEPT_EULA=Y" --rm --network=host \ + -e "PRIVACY_CONSENT=Y" \ + -v ~/docker/isaac-sim/cache/kit:/isaac-sim/kit/cache:rw \ + -v ~/docker/isaac-sim/cache/ov:/root/.cache/ov:rw \ + -v ~/docker/isaac-sim/cache/pip:/root/.cache/pip:rw \ + -v ~/docker/isaac-sim/cache/glcache:/root/.cache/nvidia/GLCache:rw \ + -v ~/docker/isaac-sim/cache/computecache:/root/.nv/ComputeCache:rw \ + -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/uw-lab:2.3.0 + +To enable rendering through X11 forwarding, run: + +.. code:: bash + + xhost + + docker run --name uw-lab --entrypoint bash -it --gpus all -e "ACCEPT_EULA=Y" --rm --network=host \ + -e "PRIVACY_CONSENT=Y" \ + -e DISPLAY \ + -v $HOME/.Xauthority:/root/.Xauthority \ + -v ~/docker/isaac-sim/cache/kit:/isaac-sim/kit/cache:rw \ + -v ~/docker/isaac-sim/cache/ov:/root/.cache/ov:rw \ + -v ~/docker/isaac-sim/cache/pip:/root/.cache/pip:rw \ + -v ~/docker/isaac-sim/cache/glcache:/root/.cache/nvidia/GLCache:rw \ + -v ~/docker/isaac-sim/cache/computecache:/root/.nv/ComputeCache:rw \ + -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/uw-lab:2.3.0 + +To run an example within the container, run: + +.. code:: bash + + ./uwlab.sh -p scripts/tutorials/00_sim/log_time.py --headless + + +.. _`NVIDIA Software License Agreement`: https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-software-license-agreement +.. _`container installation`: https://docs.isaacsim.omniverse.nvidia.com/latest/installation/install_container.html +.. _`Docker website`: https://docs.docker.com/desktop/install/linux-install/ +.. _`docker compose`: https://docs.docker.com/compose/install/linux/#install-using-the-repository +.. _`NVIDIA Container Toolkit`: https://github.com/NVIDIA/nvidia-container-toolkit +.. _`Container Toolkit website`: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html +.. _`post-installation steps`: https://docs.docker.com/engine/install/linux-postinstall/ +.. _`Isaac Sim container`: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/isaac-sim +.. _`NGC API key`: https://docs.nvidia.com/ngc/gpu-cloud/ngc-user-guide/index.html#generating-api-key +.. _`several streaming clients`: https://docs.isaacsim.omniverse.nvidia.com/latest/installation/manual_livestream_clients.html +.. _`known issue`: https://forums.developer.nvidia.com/t/unable-to-use-webrtc-when-i-run-runheadless-webrtc-sh-in-remote-headless-container/222916 +.. _`profile`: https://docs.docker.com/compose/compose-file/15-profiles/ +.. _`apt package`: https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html#install-ros-2-packages +.. _`various middleware`: https://docs.ros.org/en/humble/How-To-Guides/Working-with-multiple-RMW-implementations.html +.. _`tuned`: https://docs.ros.org/en/foxy/How-To-Guides/DDS-tuning.html diff --git a/docs/source/deployment/index.rst b/docs/source/deployment/index.rst new file mode 100644 index 0000000..2b05f27 --- /dev/null +++ b/docs/source/deployment/index.rst @@ -0,0 +1,69 @@ +.. _container-deployment: + +Container Deployment +==================== + +Docker is a tool that allows for the creation of containers, which are isolated environments that can +be used to run applications. They are useful for ensuring that an application can run on any machine +that has Docker installed, regardless of the host machine's operating system or installed libraries. + +We include a Dockerfile and docker-compose.yaml file that can be used to build a Docker image that +contains UW Lab and all of its dependencies. This image can then be used to run UW Lab in a container. +The Dockerfile is based on the Isaac Sim image provided by NVIDIA, which includes the Omniverse +application launcher and the Isaac Sim application. The Dockerfile installs UW Lab and its dependencies +on top of this image. + +Cloning the Repository +---------------------- + +Before building the container, clone the UW Lab repository (if not already done): + +.. tab-set:: + + .. tab-item:: SSH + + .. code:: bash + + git clone git@github.com:uw-lab/UWLab.git + + .. tab-item:: HTTPS + + .. code:: bash + + git clone https://github.com/uw-lab/UWLab.git + +Next Steps +---------- + +After cloning, you can choose the deployment workflow that fits your needs: + +- :doc:`docker` + + - Learn how to build, configure, and run UW Lab in Docker containers. + - Explains the repository's ``docker/`` setup, the ``container.py`` helper script, mounted volumes, + image extensions (like ROS 2), and optional CloudXR streaming support. + - Covers running pre-built UW Lab containers from NVIDIA NGC for headless training. + +- :doc:`run_docker_example` + + - Learn how to run a development workflow inside the UW Lab Docker container. + - Demonstrates building the container, entering it, executing a sample Python script (`log_time.py`), + and retrieving logs using mounted volumes. + - Highlights bind-mounted directories for live code editing and explains how to stop or remove the container + while keeping the image and artifacts. + +- :doc:`cluster` + + - Learn how to run UW Lab on high-performance computing (HPC) clusters. + - Explains how to export the Docker image to a Singularity (Apptainer) image, configure cluster-specific parameters, + and submit jobs using common workload managers (SLURM or PBS). + - Includes tested workflows for ETH Zurich's Euler cluster and IIT Genoa's Franklin cluster, + with notes on adapting to other environments. + +.. toctree:: + :maxdepth: 1 + :hidden: + + docker + run_docker_example + cluster diff --git a/docs/source/deployment/run_docker_example.rst b/docs/source/deployment/run_docker_example.rst new file mode 100644 index 0000000..761c1f2 --- /dev/null +++ b/docs/source/deployment/run_docker_example.rst @@ -0,0 +1,104 @@ +Running an example with Docker +============================== + +From the root of the UW Lab repository, the ``docker`` directory contains all the Docker relevant files. These include the three files +(**Dockerfile**, **docker-compose.yaml**, **.env**) which are used by Docker, and an additional script that we use to interface with them, +**container.py**. + +In this tutorial, we will learn how to use the UW Lab Docker container for development. For a detailed description of the Docker setup, +including installation and obtaining access to an Isaac Sim image, please reference the :ref:`deployment-docker`. For a description +of Docker in general, please refer to `their official documentation `_. + + +Building the Container +~~~~~~~~~~~~~~~~~~~~~~ + +To build the UW Lab container from the root of the UW Lab repository, we will run the following: + + +.. code-block:: console + + python docker/container.py start + + +The terminal will first pull the base IsaacSim image, build the UW Lab image's additional layers on top of it, and run the UW Lab container. +This should take several minutes for the first build but will be shorter in subsequent runs as Docker's caching prevents repeated work. +If we run the command ``docker container ls`` on the terminal, the output will list the containers that are running on the system. If +everything has been set up correctly, a container with the ``NAME`` **uw-lab-base** should appear, similar to below: + + +.. code-block:: console + + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 483d1d5e2def uw-lab-base "bash" 30 seconds ago Up 30 seconds uw-lab-base + + +Once the container is up and running, we can enter it from our terminal. + +.. code-block:: console + + python docker/container.py enter + + +On entering the UW Lab container, we are in the terminal as the superuser, ``root``. This environment contains a copy of the +UW Lab repository, but also has access to the directories and libraries of Isaac Sim. We can run experiments from this environment +using a few convenient aliases that have been put into the ``root`` **.bashrc**. For instance, we have made the **uwlab.sh** script +usable from anywhere by typing its alias ``uwlab``. + +Additionally in the container, we have `bind mounted`_ the ``UWLab/source`` directory from the +host machine. This means that if we modify files under this directory from an editor on the host machine, the changes are +reflected immediately within the container without requiring us to rebuild the Docker image. + +We will now run a sample script from within the container to demonstrate how to extract artifacts +from the UW Lab Docker container. + +Executing the Script +~~~~~~~~~~~~~~~~~~~~ + +We will execute the script to produce a log, adding a ``--headless`` flag to our execution to prevent a GUI: + +.. code-block:: bash + + uwlab -p scripts/tutorials/00_sim/log_time.py --headless + + +Now ``log.txt`` will have been produced at ``/workspace/uwlab/logs/docker_tutorial``. If we exit the container +by typing ``exit``, we will return to ``UWLab/docker`` in our host terminal environment. We can then enter +the following command to retrieve our logs from the Docker container and put them on our host machine: + +.. code-block:: console + + ./container.py copy + + +We will see a terminal readout reporting the artifacts we have retrieved from the container. If we navigate to +``/uwlab/docker/artifacts/logs/docker_tutorial``, we will see a copy of the ``log.txt`` file which was produced +by the script above. + +Each of the directories under ``artifacts`` corresponds to Docker `volumes`_ mapped to directories +within the container and the ``container.py copy`` command copies them from those `volumes`_ to these directories. + +We could return to the UW Lab Docker terminal environment by running ``container.py enter`` again, +but we have retrieved our logs and wish to go inspect them. We can stop the UW Lab Docker container with the following command: + +.. code-block:: console + + ./container.py stop + + +This will bring down the Docker UW Lab container. The image will persist and remain available for further use, as will +the contents of any `volumes`_. If we wish to free up the disk space taken by the image, (~20.1GB), and do not mind repeating +the build process when we next run ``./container.py start``, we may enter the following command to delete the **uw-lab-base** image: + +.. code-block:: console + + docker image rm uw-lab-base + +A subsequent run of ``docker image ls`` will show that the image tagged **uw-lab-base** is now gone. We can repeat the process for the +underlying NVIDIA container if we wish to free up more space. If a more powerful method of freeing resources from Docker is desired, +please consult the documentation for the `docker prune`_ commands. + + +.. _volumes: https://docs.docker.com/storage/volumes/ +.. _bind mounted: https://docs.docker.com/storage/bind-mounts/ +.. _docker prune: https://docs.docker.com/config/pruning/ diff --git a/docs/source/overview/developer-guide/development.rst b/docs/source/overview/developer-guide/development.rst new file mode 100644 index 0000000..4d586d2 --- /dev/null +++ b/docs/source/overview/developer-guide/development.rst @@ -0,0 +1,164 @@ +Extension Development +======================= + +Everything in Omniverse is either an extension or a collection of extensions (an application). They are +modularized packages that form the atoms of the Omniverse ecosystem. Each extension +provides a set of functionalities that can be used by other extensions or +standalone applications. A folder is recognized as an extension if it contains +an ``extension.toml`` file in the ``config`` directory. More information on extensions can be found in the +`Omniverse documentation `__. + +Each extension in UW Lab is written as a python package and follows the following structure: + +.. code:: bash + + + β”œβ”€β”€ config + β”‚Β Β  └── extension.toml + β”œβ”€β”€ docs + β”‚Β Β  β”œβ”€β”€ CHANGELOG.md + β”‚Β Β  └── README.md + β”œβ”€β”€ + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ .... + β”‚ └── scripts + β”œβ”€β”€ setup.py + └── tests + +The ``config/extension.toml`` file contains the metadata of the extension. This +includes the name, version, description, dependencies, etc. This information is used +by the Omniverse API to load the extension. The ``docs`` directory contains the documentation +for the extension with more detailed information about the extension and a CHANGELOG +file that contains the changes made to the extension in each version. + +The ```` directory contains the main python package for the extension. +It may also contain the ``scripts`` directory for keeping python-based applications +that are loaded into Omniverse when the extension is enabled using the +`Extension Manager `__. + +More specifically, when an extension is enabled, the python module specified in the +``config/extension.toml`` file is loaded and scripts that contain children of the +:class:`omni.ext.IExt` class are executed. + +.. code:: python + + import omni.ext + + class MyExt(omni.ext.IExt): + """My extension application.""" + + def on_startup(self, ext_id): + """Called when the extension is loaded.""" + pass + + def on_shutdown(self): + """Called when the extension is unloaded. + + It releases all references to the extension and cleans up any resources. + """ + pass + +While loading extensions into Omniverse happens automatically, using the python package +in standalone applications requires additional steps. To simplify the build process and +avoid the need to understand the `premake `__ +build system used by Omniverse, we directly use the `setuptools `__ +python package to build the python module provided by the extensions. This is done by the +``setup.py`` file in the extension directory. + +.. note:: + + The ``setup.py`` file is not required for extensions that are only loaded into Omniverse + using the `Extension Manager `__. + +Lastly, the ``tests`` directory contains the unit tests for the extension. These are written +using the `unittest `__ framework. It is +important to note that Omniverse also provides a similar +`testing framework `__. +However, it requires going through the build process and does not support testing of the python module in +standalone applications. + +Custom Extension Dependency Management +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Certain extensions may have dependencies which require the installation of additional packages before the extension +can be used. While Python dependencies are handled by the `setuptools `__ +package and specified in the ``setup.py`` file, non-Python dependencies such as `ROS `__ +packages or `apt `__ packages are not handled by setuptools. +Handling these kinds of dependencies requires an additional procedure. + +There are two types of dependencies that can be specified in the ``extension.toml`` file +under the ``isaac_lab_settings`` section: + +1. **apt_deps**: A list of apt packages that need to be installed. These are installed using the + `apt `__ package manager. +2. **ros_ws**: The path to the ROS workspace that contains the ROS packages. These are installed using + the `rosdep `__ dependency manager. + +As an example, the following ``extension.toml`` file specifies the dependencies for the extension: + +.. code-block:: toml + + [isaac_lab_settings] + # apt dependencies + apt_deps = ["libboost-all-dev"] + + # ROS workspace + # note: if this path is relative, it is relative to the extension directory's root + ros_ws = "/home/user/catkin_ws" + +These dependencies are installed using the ``install_deps.py`` script provided in the ``tools`` directory. +To install all dependencies for all extensions, run the following command: + +.. code-block:: bash + + # execute from the root of the repository + # the script expects the type of dependencies to install and the path to the extensions directory + # available types are: 'apt', 'rosdep' and 'all' + python tools/install_deps.py all ${UWLAB_PATH}/source + +.. note:: + Currently, this script is automatically executed during the build process of the ``Dockerfile.base`` + and ``Dockerfile.ros2``. This ensures that all the 'apt' and 'rosdep' dependencies are installed + before building the extensions respectively. + + +Standalone applications +~~~~~~~~~~~~~~~~~~~~~~~ + +In a typical Omniverse workflow, the simulator is launched first and then the extensions are +enabled. The loading of python modules and other python applications happens automagically, under the hood, and while this is the recommended +workflow, it is not always possible. + +For example, consider robot reinforcement learning. It is essential to have complete control over the simulation step +and when things update instead of asynchronously waiting for the result. In +such cases, we require direct control of the simulation, and so it is necessary to write a standalone application. These applications are functionally similar in that they launch the simulator using the :class:`~uwlab.app.AppLauncher` and +then control the simulation directly through the :class:`~isaaclab.sim.SimulationContext`. In these cases, python modules from extensions **must** be imported after the app is launched. Doing so before the app is launched will cause missing module errors. + +The following snippet shows how to write a standalone application: + +.. code:: python + + """Launch Isaac Sim Simulator first.""" + + from isaaclab.app import AppLauncher + + # launch omniverse app + app_launcher = AppLauncher(headless=False) + simulation_app = app_launcher.app + + + """Rest everything follows.""" + + from isaaclab.sim import SimulationContext + + if __name__ == "__main__": + # get simulation context + simulation_context = SimulationContext() + # reset and play simulation + simulation_context.reset() + # step simulation + simulation_context.step() + # stop simulation + simulation_context.stop() + + # close the simulation diff --git a/docs/source/overview/developer-guide/index.rst b/docs/source/overview/developer-guide/index.rst new file mode 100644 index 0000000..59f603f --- /dev/null +++ b/docs/source/overview/developer-guide/index.rst @@ -0,0 +1,15 @@ +Developer's Guide +================= + +For development, we suggest using `Microsoft Visual Studio Code +(VSCode) `__. This is also suggested by +NVIDIA Omniverse and there exists tutorials on how to `debug Omniverse +extensions `__ +using VSCode. + +.. toctree:: + :maxdepth: 1 + + VS Code + repo_structure + development diff --git a/docs/source/overview/developer-guide/repo_structure.rst b/docs/source/overview/developer-guide/repo_structure.rst new file mode 100644 index 0000000..6281d6a --- /dev/null +++ b/docs/source/overview/developer-guide/repo_structure.rst @@ -0,0 +1,69 @@ +Repository organization +----------------------- + +.. code-block:: bash + + UWLab + β”œβ”€β”€ .vscode + β”œβ”€β”€ .flake8 + β”œβ”€β”€ CONTRIBUTING.md + β”œβ”€β”€ CONTRIBUTORS.md + β”œβ”€β”€ LICENSE + β”œβ”€β”€ uwlab.bat + β”œβ”€β”€ uwlab.sh + β”œβ”€β”€ pyproject.toml + β”œβ”€β”€ README.md + β”œβ”€β”€ docs + β”œβ”€β”€ docker + β”œβ”€β”€ source + β”‚Β Β  β”œβ”€β”€ uwlab + β”‚Β Β  β”œβ”€β”€ uwlab_assets + β”‚Β Β  β”œβ”€β”€ uwlab_mimic + β”‚Β Β  β”œβ”€β”€ uwlab_rl + β”‚Β Β  └── uwlab_tasks + β”œβ”€β”€ scripts + β”‚Β Β  β”œβ”€β”€ benchmarks + β”‚Β Β  β”œβ”€β”€ demos + β”‚Β Β  β”œβ”€β”€ environments + β”‚Β Β  β”œβ”€β”€ imitation_learning + β”‚Β Β  β”œβ”€β”€ reinforcement_learning + β”‚Β Β  β”œβ”€β”€ tools + β”‚Β Β  β”œβ”€β”€ tutorials + β”œβ”€β”€ tools + └── VERSION + +UW Lab is built on the same back end as Isaac Sim. As such, it exists as a collection of **extensions** that can be assembled into **applications**. +The ``source`` directory contains the majority of the code in the repository and the specific extensions that compose Isaac lab, while ``scripts`` containing python scripts for launching customized standalone apps (Like our workflows). +These are the two primary ways of interacting with the simulation and Isaac lab supports both! +Checkout this `Isaac Sim introduction to workflows `__ for more details. + +Extensions +~~~~~~~~~~ + +The extensions that compose UW Lab are kept in the ``source`` directory. To simplify the build process, UW Lab directly use `setuptools `__. It is strongly recommend that you adhere to this process if you create your own extensions using UW Lab. + +The extensions are organized as follows: + +* **uwlab**: Contains the core interface extension for UW Lab. This provides the main modules for actuators, + objects, robots and sensors. +* **uwlab_assets**: Contains the extension with pre-configured assets for UW Lab. +* **uwlab_tasks**: Contains the extension with pre-configured environments for UW Lab. +* **uwlab_mimic**: Contains APIs and pre-configured environments for data generation for imitation learning. +* **uwlab_rl**: Contains wrappers for using the above environments with different reinforcement learning agents. + + +Standalone +~~~~~~~~~~ + +The ``scripts`` directory contains various standalone applications written in python. +They are structured as follows: + +* **benchmarks**: Contains scripts for benchmarking different framework components. +* **demos**: Contains various demo applications that showcase the core framework :mod:`uwlab`. +* **environments**: Contains applications for running environments defined in :mod:`uwlab_tasks` with + different agents. These include a random policy, zero-action policy, teleoperation or scripted state machines. +* **tools**: Contains applications for using the tools provided by the framework. These include converting assets, + generating datasets, etc. +* **tutorials**: Contains step-by-step tutorials for using the APIs provided by the framework. +* **workflows**: Contains applications for using environments with various learning-based frameworks. These include different + reinforcement learning or imitation learning libraries. diff --git a/docs/source/overview/developer-guide/vs_code.rst b/docs/source/overview/developer-guide/vs_code.rst new file mode 100644 index 0000000..eb19b5a --- /dev/null +++ b/docs/source/overview/developer-guide/vs_code.rst @@ -0,0 +1,77 @@ +.. _setup-vs-code: + +Setting up Visual Studio Code +----------------------------- + +**This is optional. You do not need to use VScode to use UW Lab** + +`Visual Studio Code `_ has proven an invaluable tool for the development of UW Lab. The UW Lab repository includes the VSCode files for setting up your development environment. These are included in the ``.vscode`` directory and include the following files: + +.. code-block:: bash + + .vscode + β”œβ”€β”€ tools + β”‚Β Β  β”œβ”€β”€ launch.template.json + β”‚Β Β  β”œβ”€β”€ settings.template.json + β”‚Β Β  └── setup_vscode.py + β”œβ”€β”€ extensions.json + β”œβ”€β”€ launch.json # <- this is generated by setup_vscode.py + β”œβ”€β”€ settings.json # <- this is generated by setup_vscode.py + └── tasks.json + + +.. attention:: + + The following instructions on setting up Visual Studio Code only work with + :ref:`Isaac Sim Binaries Installation ` and not with + :ref:`Pip Installation `. + + +To setup the IDE, please follow these instructions: + +1. Open the ``UWLab`` directory on Visual Studio Code IDE +2. Run VSCode `Tasks `__, by + pressing ``Ctrl+Shift+P``, selecting ``Tasks: Run Task`` and running the + ``setup_python_env`` in the drop down menu. + + .. image:: ../../_static/vscode_tasks.png + :width: 600px + :align: center + :alt: VSCode Tasks + + +.. note:: + If this is your first time running tasks in VS Code, you may be prompted to select how to handle warnings. Simply follow + the prompts until the task window closes. + +If everything executes correctly, it should create the following files: + +* ``.vscode/launch.json``: Contains the launch configurations for debugging python code. +* ``.vscode/settings.json``: Contains the settings for the python interpreter and the python environment. + +For more information on VSCode support for Omniverse, please refer to the +following links: + +* `Isaac Sim VSCode support `__ + + +Configuring the python interpreter +---------------------------------- + +In the provided configuration, we set the default python interpreter to use the +python executable provided by Omniverse. This is specified in the +``.vscode/settings.json`` file: + +.. code-block:: json + + { + "python.defaultInterpreterPath": "${workspaceFolder}/_isaac_sim/python.sh", + } + +If you want to use a different python interpreter (for instance, from your conda or uv environment), +you need to change the python interpreter used by selecting and activating the python interpreter +of your choice in the bottom left corner of VSCode, or opening the command palette (``Ctrl+Shift+P``) +and selecting ``Python: Select Interpreter``. + +For more information on how to set python interpreter for VSCode, please +refer to the `VSCode documentation `_. diff --git a/docs/source/overview/isaac_environments.rst b/docs/source/overview/isaac_environments.rst index 33a4ffa..e601fa2 100644 --- a/docs/source/overview/isaac_environments.rst +++ b/docs/source/overview/isaac_environments.rst @@ -1,9 +1,9 @@ .. _environments: -Available Isaac Environments -============================ +Available Environments +====================== -The following lists comprises of all the RL tasks implementations that are available in Isaac Lab. +The following lists comprises of all the RL and IL tasks implementations that are available in UW Lab. While we try to keep this list up-to-date, you can always get the latest list of environments by running the following command: @@ -15,17 +15,17 @@ running the following command: .. code:: bash - ./isaaclab.sh -p scripts/environments/list_envs.py + ./uwlab.sh -p scripts/environments/list_envs.py .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - isaaclab.bat -p scripts\environments\list_envs.py + uwlab.bat -p scripts\environments\list_envs.py 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! +you would like to add to UW Lab, please feel free to open a pull request! Single-agent ------------ @@ -54,7 +54,7 @@ Classic environments that are based on IsaacGymEnvs implementation of MuJoCo-sty | | |cartpole-direct-link| | | +------------------+-----------------------------+-------------------------------------------------------------------------+ | |cartpole| | |cartpole-rgb-link| | Move the cart to keep the pole upwards in the classic cartpole control | - | | | and perceptive inputs | + | | | and perceptive inputs. Requires running with ``--enable_cameras``. | | | |cartpole-depth-link| | | | | | | | | |cartpole-rgb-direct-link| | | @@ -63,27 +63,27 @@ Classic environments that are based on IsaacGymEnvs implementation of MuJoCo-sty +------------------+-----------------------------+-------------------------------------------------------------------------+ | |cartpole| | |cartpole-resnet-link| | Move the cart to keep the pole upwards in the classic cartpole control | | | | based off of features extracted from perceptive inputs with pre-trained | - | | |cartpole-theia-link| | frozen vision encoders | + | | |cartpole-theia-link| | frozen vision encoders. Requires running with ``--enable_cameras``. | +------------------+-----------------------------+-------------------------------------------------------------------------+ .. |humanoid| image:: ../_static/tasks/classic/humanoid.jpg .. |ant| image:: ../_static/tasks/classic/ant.jpg .. |cartpole| image:: ../_static/tasks/classic/cartpole.jpg -.. |humanoid-link| replace:: `Isaac-Humanoid-v0 `__ -.. |ant-link| replace:: `Isaac-Ant-v0 `__ -.. |cartpole-link| replace:: `Isaac-Cartpole-v0 `__ -.. |cartpole-rgb-link| replace:: `Isaac-Cartpole-RGB-v0 `__ -.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-v0 `__ -.. |cartpole-resnet-link| replace:: `Isaac-Cartpole-RGB-ResNet18-v0 `__ -.. |cartpole-theia-link| replace:: `Isaac-Cartpole-RGB-TheiaTiny-v0 `__ +.. |humanoid-link| replace:: `Isaac-Humanoid-v0 `__ +.. |ant-link| replace:: `Isaac-Ant-v0 `__ +.. |cartpole-link| replace:: `Isaac-Cartpole-v0 `__ +.. |cartpole-rgb-link| replace:: `Isaac-Cartpole-RGB-v0 `__ +.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-v0 `__ +.. |cartpole-resnet-link| replace:: `Isaac-Cartpole-RGB-ResNet18-v0 `__ +.. |cartpole-theia-link| replace:: `Isaac-Cartpole-RGB-TheiaTiny-v0 `__ -.. |humanoid-direct-link| replace:: `Isaac-Humanoid-Direct-v0 `__ -.. |ant-direct-link| replace:: `Isaac-Ant-Direct-v0 `__ -.. |cartpole-direct-link| replace:: `Isaac-Cartpole-Direct-v0 `__ -.. |cartpole-rgb-direct-link| replace:: `Isaac-Cartpole-RGB-Camera-Direct-v0 `__ -.. |cartpole-depth-direct-link| replace:: `Isaac-Cartpole-Depth-Camera-Direct-v0 `__ +.. |humanoid-direct-link| replace:: `Isaac-Humanoid-Direct-v0 `__ +.. |ant-direct-link| replace:: `Isaac-Ant-Direct-v0 `__ +.. |cartpole-direct-link| replace:: `Isaac-Cartpole-Direct-v0 `__ +.. |cartpole-rgb-direct-link| replace:: `Isaac-Cartpole-RGB-Camera-Direct-v0 `__ +.. |cartpole-depth-direct-link| replace:: `Isaac-Cartpole-Depth-Camera-Direct-v0 `__ Manipulation ~~~~~~~~~~~~ @@ -100,57 +100,119 @@ for the lift-cube environment: .. table:: :widths: 33 37 30 - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +====================+=========================+=============================================================================+ - | |reach-franka| | |reach-franka-link| | Move the end-effector to a sampled target pose with the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |reach-ur10| | |reach-ur10-link| | Move the end-effector to a sampled target pose with the UR10 robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |lift-cube| | |lift-cube-link| | Pick a cube and bring it to a sampled target position with the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |stack-cube| | |stack-cube-link| | Stack three cubes (bottom to top: blue, red, green) with the Franka robot | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |cabi-franka| | |cabi-franka-link| | Grasp the handle of a cabinet's drawer and open it with the Franka robot | - | | | | - | | |franka-direct-link| | | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |cube-allegro| | |cube-allegro-link| | In-hand reorientation of a cube using Allegro hand | - | | | | - | | |allegro-direct-link| | | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |cube-shadow| | |cube-shadow-link| | In-hand reorientation of a cube using Shadow hand | - | | | | - | | |cube-shadow-ff-link| | | - | | | | - | | |cube-shadow-lstm-link| | | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ - | |cube-shadow| | |cube-shadow-vis-link| | In-hand reorientation of a cube using Shadow hand using perceptive inputs | - +--------------------+-------------------------+-----------------------------------------------------------------------------+ + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +=========================+==============================+=============================================================================+ + | |reach-franka| | |reach-franka-link| | Move the end-effector to a sampled target pose with the Franka robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |reach-ur10| | |reach-ur10-link| | Move the end-effector to a sampled target pose with the UR10 robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |deploy-reach-ur10e| | |deploy-reach-ur10e-link| | Move the end-effector to a sampled target pose with the UR10e robot | + | | | This policy has been deployed to a real robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |lift-cube| | |lift-cube-link| | Pick a cube and bring it to a sampled target position with the Franka robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |stack-cube| | |stack-cube-link| | Stack three cubes (bottom to top: blue, red, green) with the Franka robot. | + | | | Blueprint env used for the NVIDIA Isaac GR00T blueprint for synthetic | + | | |stack-cube-bp-link| | manipulation motion generation | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |surface-gripper| | |long-suction-link| | Stack three cubes (bottom to top: blue, red, green) | + | | | with the UR10 arm and long surface gripper | + | | |short-suction-link| | or short surface gripper. | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cabi-franka| | |cabi-franka-link| | Grasp the handle of a cabinet's drawer and open it with the Franka robot | + | | | | + | | |franka-direct-link| | | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cube-allegro| | |cube-allegro-link| | In-hand reorientation of a cube using Allegro hand | + | | | | + | | |allegro-direct-link| | | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cube-shadow| | |cube-shadow-link| | In-hand reorientation of a cube using Shadow hand | + | | | | + | | |cube-shadow-ff-link| | | + | | | | + | | |cube-shadow-lstm-link| | | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cube-shadow| | |cube-shadow-vis-link| | In-hand reorientation of a cube using Shadow hand using perceptive inputs. | + | | | Requires running with ``--enable_cameras``. | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |gr1_pick_place| | |gr1_pick_place-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |gr1_pp_waist| | |gr1_pp_waist-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | + | | | with waist degrees-of-freedom enables that provides a wider reach space. | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |g1_pick_place| | |g1_pick_place-link| | Pick up and place an object in a basket with a Unitree G1 humanoid robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |g1_pick_place_fixed| | |g1_pick_place_fixed-link| | Pick up and place an object in a basket with a Unitree G1 humanoid robot | + | | | with three-fingered hands. Robot is set up with the base fixed in place. | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |g1_pick_place_lm| | |g1_pick_place_lm-link| | Pick up and place an object in a basket with a Unitree G1 humanoid robot | + | | | with three-fingered hands and in-place locomanipulation capabilities | + | | | enabled (i.e. Robot lower body balances in-place while upper body is | + | | | controlled via Inverse Kinematics). | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |kuka-allegro-lift| | |kuka-allegro-lift-link| | Pick up a primitive shape on the table and lift it to target position | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |kuka-allegro-reorient| | |kuka-allegro-reorient-link| | Pick up a primitive shape on the table and orient it to target pose | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |galbot_stack| | |galbot_stack-link| | Stack three cubes (bottom to top: blue, red, green) with the left arm of | + | | | a Galbot humanoid robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |agibot_place_mug| | |agibot_place_mug-link| | Pick up and place a mug upright with a Agibot A2D humanoid robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |agibot_place_toy| | |agibot_place_toy-link| | Pick up and place an object in a box with a Agibot A2D humanoid robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ .. |reach-franka| image:: ../_static/tasks/manipulation/franka_reach.jpg .. |reach-ur10| image:: ../_static/tasks/manipulation/ur10_reach.jpg +.. |deploy-reach-ur10e| image:: ../_static/tasks/manipulation/ur10e_reach.jpg .. |lift-cube| image:: ../_static/tasks/manipulation/franka_lift.jpg .. |cabi-franka| image:: ../_static/tasks/manipulation/franka_open_drawer.jpg .. |cube-allegro| image:: ../_static/tasks/manipulation/allegro_cube.jpg .. |cube-shadow| image:: ../_static/tasks/manipulation/shadow_cube.jpg .. |stack-cube| image:: ../_static/tasks/manipulation/franka_stack.jpg +.. |gr1_pick_place| image:: ../_static/tasks/manipulation/gr-1_pick_place.jpg +.. |g1_pick_place| image:: ../_static/tasks/manipulation/g1_pick_place.jpg +.. |g1_pick_place_fixed| image:: ../_static/tasks/manipulation/g1_pick_place_fixed_base.jpg +.. |g1_pick_place_lm| image:: ../_static/tasks/manipulation/g1_pick_place_locomanipulation.jpg +.. |surface-gripper| image:: ../_static/tasks/manipulation/ur10_stack_surface_gripper.jpg +.. |gr1_pp_waist| image:: ../_static/tasks/manipulation/gr-1_pick_place_waist.jpg +.. |galbot_stack| image:: ../_static/tasks/manipulation/galbot_stack_cube.jpg +.. |agibot_place_mug| image:: ../_static/tasks/manipulation/agibot_place_mug.jpg +.. |agibot_place_toy| image:: ../_static/tasks/manipulation/agibot_place_toy.jpg +.. |kuka-allegro-lift| image:: ../_static/tasks/manipulation/kuka_allegro_lift.jpg +.. |kuka-allegro-reorient| image:: ../_static/tasks/manipulation/kuka_allegro_reorient.jpg + +.. |reach-franka-link| replace:: `Isaac-Reach-Franka-v0 `__ +.. |reach-ur10-link| replace:: `Isaac-Reach-UR10-v0 `__ +.. |deploy-reach-ur10e-link| replace:: `Isaac-Deploy-Reach-UR10e-v0 `__ +.. |lift-cube-link| replace:: `Isaac-Lift-Cube-Franka-v0 `__ +.. |lift-cube-ik-abs-link| replace:: `Isaac-Lift-Cube-Franka-IK-Abs-v0 `__ +.. |lift-cube-ik-rel-link| replace:: `Isaac-Lift-Cube-Franka-IK-Rel-v0 `__ +.. |cabi-franka-link| replace:: `Isaac-Open-Drawer-Franka-v0 `__ +.. |franka-direct-link| replace:: `Isaac-Franka-Cabinet-Direct-v0 `__ +.. |cube-allegro-link| replace:: `Isaac-Repose-Cube-Allegro-v0 `__ +.. |allegro-direct-link| replace:: `Isaac-Repose-Cube-Allegro-Direct-v0 `__ +.. |stack-cube-link| replace:: `Isaac-Stack-Cube-Franka-v0 `__ +.. |stack-cube-bp-link| replace:: `Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-v0 `__ +.. |gr1_pick_place-link| replace:: `Isaac-PickPlace-GR1T2-Abs-v0 `__ +.. |g1_pick_place-link| replace:: `Isaac-PickPlace-G1-InspireFTP-Abs-v0 `__ +.. |g1_pick_place_fixed-link| replace:: `Isaac-PickPlace-FixedBaseUpperBodyIK-G1-Abs-v0 `__ +.. |g1_pick_place_lm-link| replace:: `Isaac-PickPlace-Locomanipulation-G1-Abs-v0 `__ +.. |long-suction-link| replace:: `Isaac-Stack-Cube-UR10-Long-Suction-IK-Rel-v0 `__ +.. |short-suction-link| replace:: `Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0 `__ +.. |gr1_pp_waist-link| replace:: `Isaac-PickPlace-GR1T2-WaistEnabled-Abs-v0 `__ +.. |galbot_stack-link| replace:: `Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 `__ +.. |kuka-allegro-lift-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Lift-v0 `__ +.. |kuka-allegro-reorient-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 `__ +.. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ +.. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ +.. |cube-shadow-lstm-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 `__ +.. |cube-shadow-vis-link| replace:: `Isaac-Repose-Cube-Shadow-Vision-Direct-v0 `__ +.. |agibot_place_mug-link| replace:: `Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 `__ +.. |agibot_place_toy-link| replace:: `Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 `__ -.. |reach-franka-link| replace:: `Isaac-Reach-Franka-v0 `__ -.. |reach-ur10-link| replace:: `Isaac-Reach-UR10-v0 `__ -.. |lift-cube-link| replace:: `Isaac-Lift-Cube-Franka-v0 `__ -.. |lift-cube-ik-abs-link| replace:: `Isaac-Lift-Cube-Franka-IK-Abs-v0 `__ -.. |lift-cube-ik-rel-link| replace:: `Isaac-Lift-Cube-Franka-IK-Rel-v0 `__ -.. |cabi-franka-link| replace:: `Isaac-Open-Drawer-Franka-v0 `__ -.. |franka-direct-link| replace:: `Isaac-Franka-Cabinet-Direct-v0 `__ -.. |cube-allegro-link| replace:: `Isaac-Repose-Cube-Allegro-v0 `__ -.. |allegro-direct-link| replace:: `Isaac-Repose-Cube-Allegro-Direct-v0 `__ -.. |stack-cube-link| replace:: `Isaac-Stack-Cube-Franka-v0 `__ - -.. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ -.. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ -.. |cube-shadow-lstm-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 `__ -.. |cube-shadow-vis-link| replace:: `Isaac-Repose-Cube-Shadow-Vision-Direct-v0 `__ Contact-rich Manipulation ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -181,9 +243,98 @@ For example: .. |factory-gear| image:: ../_static/tasks/factory/gear_mesh.jpg .. |factory-nut| image:: ../_static/tasks/factory/nut_thread.jpg -.. |factory-peg-link| replace:: `Isaac-Factory-PegInsert-Direct-v0 `__ -.. |factory-gear-link| replace:: `Isaac-Factory-GearMesh-Direct-v0 `__ -.. |factory-nut-link| replace:: `Isaac-Factory-NutThread-Direct-v0 `__ +.. |factory-peg-link| replace:: `Isaac-Factory-PegInsert-Direct-v0 `__ +.. |factory-gear-link| replace:: `Isaac-Factory-GearMesh-Direct-v0 `__ +.. |factory-nut-link| replace:: `Isaac-Factory-NutThread-Direct-v0 `__ + +AutoMate +~~~~~~~~ + +Environments based on 100 diverse assembly tasks, each involving the insertion of a plug into a socket. These tasks share a common configuration and differ by th geometry and properties of the parts. + +You can switch between tasks by specifying the corresponding asset ID. Available asset IDs include: + +'00004', '00007', '00014', '00015', '00016', '00021', '00028', '00030', '00032', '00042', '00062', '00074', '00077', '00078', '00081', '00083', '00103', '00110', '00117', '00133', '00138', '00141', '00143', '00163', '00175', '00186', '00187', '00190', '00192', '00210', '00211', '00213', '00255', '00256', '00271', '00293', '00296', '00301', '00308', '00318', '00319', '00320', '00329', '00340', '00345', '00346', '00360', '00388', '00410', '00417', '00422', '00426', '00437', '00444', '00446', '00470', '00471', '00480', '00486', '00499', '00506', '00514', '00537', '00553', '00559', '00581', '00597', '00614', '00615', '00638', '00648', '00649', '00652', '00659', '00681', '00686', '00700', '00703', '00726', '00731', '00741', '00755', '00768', '00783', '00831', '00855', '00860', '00863', '01026', '01029', '01036', '01041', '01053', '01079', '01092', '01102', '01125', '01129', '01132', '01136'. + +We provide environments for both disassembly and assembly. + +.. attention:: + + CUDA is recommended for running the AutoMate environments with 570 drivers. If running with Nvidia driver 570 on Linux with architecture x86_64, we follow the below steps to install CUDA 12.8. This allows for computing rewards in AutoMate environments with CUDA. If you have a different operation system or architecture, please refer to the `CUDA installation page `_ for additional instruction. + + .. code-block:: bash + + wget https://developer.download.nvidia.com/compute/cuda/12.8.0/local_installers/cuda_12.8.0_570.86.10_linux.run + sudo sh cuda_12.8.0_570.86.10_linux.run --toolkit + + When using conda, cuda toolkit can be installed with: + + .. code-block:: bash + + conda install cudatoolkit + + With 580 drivers and CUDA 13, we are currently unable to enable CUDA for computing the rewards. The code automatically fallbacks to CPU, resulting in slightly slower performance. + +* |disassembly-link|: The plug starts inserted in the socket. A low-level controller lifts the plug out and moves it to a random position. This process is purely scripted and does not involve any learned policy. Therefore, it does not require policy training or evaluation. The resulting trajectories serve as demonstrations for the reverse process, i.e., learning to assemble. To run disassembly for a specific task: ``python source/uwlab_tasks/uwlab_tasks/direct/automate/run_disassembly_w_id.py --assembly_id=ASSEMBLY_ID --disassembly_dir=DISASSEMBLY_DIR``. All generated trajectories are saved to a local directory ``DISASSEMBLY_DIR``. +* |assembly-link|: The goal is to insert the plug into the socket. You can use this environment to train a policy via reinforcement learning or evaluate a pre-trained checkpoint. + + * To train an assembly policy, we run the command ``python source/uwlab_tasks/uwlab_tasks/direct/automate/run_w_id.py --assembly_id=ASSEMBLY_ID --train``. We can customize the training process using the optional flags: ``--headless`` to run without opening the GUI windows, ``--max_iterations=MAX_ITERATIONS`` to set the number of training iterations, ``--num_envs=NUM_ENVS`` to set the number of parallel environments during training, ``--seed=SEED`` to assign the random seed. The policy checkpoints will be saved automatically during training in the directory ``logs/rl_games/Assembly/test``. + * To evaluate an assembly policy, we run the command ``python source/uwlab_tasks/uwlab_tasks/direct/automate/run_w_id.py --assembly_id=ASSEMBLY_ID --checkpoint=CHECKPOINT --log_eval``. The evaluation results are stored in ``evaluation_{ASSEMBLY_ID}.h5``. + +.. table:: + :widths: 33 37 30 + + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +====================+=========================+=============================================================================+ + | |disassembly| | |disassembly-link| | Lift a plug out of the socket with the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |assembly| | |assembly-link| | Insert a plug into its corresponding socket with the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + +.. |assembly| image:: ../_static/tasks/automate/00004.jpg +.. |disassembly| image:: ../_static/tasks/automate/01053_disassembly.jpg + +.. |assembly-link| replace:: `Isaac-AutoMate-Assembly-Direct-v0 `__ +.. |disassembly-link| replace:: `Isaac-AutoMate-Disassembly-Direct-v0 `__ + +FORGE +~~~~~~~~ + +FORGE environments extend Factory environments with: + +* Force sensing: Add observations for force experienced by the end-effector. +* Excessive force penalty: Add an option to penalize the agent for excessive contact forces. +* Dynamics randomization: Randomize controller gains, asset properties (friction, mass), and dead-zone. +* Success prediction: Add an extra action that predicts task success. + +These tasks share the same task configurations and control options. You can switch between them by specifying the task name. + +* |forge-peg-link|: Peg insertion with the Franka arm +* |forge-gear-link|: Gear meshing with the Franka arm +* |forge-nut-link|: Nut-Bolt fastening with the Franka arm + +.. table:: + :widths: 33 37 30 + + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +====================+=========================+=============================================================================+ + | |forge-peg| | |forge-peg-link| | Insert peg into the socket with the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |forge-gear| | |forge-gear-link| | Insert and mesh gear into the base with other gears, using the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |forge-nut| | |forge-nut-link| | Thread the nut onto the first 2 threads of the bolt, using the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + +.. |forge-peg| image:: ../_static/tasks/factory/peg_insert.jpg +.. |forge-gear| image:: ../_static/tasks/factory/gear_mesh.jpg +.. |forge-nut| image:: ../_static/tasks/factory/nut_thread.jpg + +.. |forge-peg-link| replace:: `Isaac-Forge-PegInsert-Direct-v0 `__ +.. |forge-gear-link| replace:: `Isaac-Forge-GearMesh-Direct-v0 `__ +.. |forge-nut-link| replace:: `Isaac-Forge-NutThread-Direct-v0 `__ + Locomotion ~~~~~~~~~~ @@ -234,36 +385,45 @@ Environments based on legged locomotion tasks. +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ | |velocity-rough-g1| | |velocity-rough-g1-link| | Track a velocity command on rough terrain with the Unitree G1 robot | +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-digit| | |velocity-flat-digit-link| | Track a velocity command on flat terrain with the Agility Digit robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-digit| | |velocity-rough-digit-link| | Track a velocity command on rough terrain with the Agility Digit robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |tracking-loco-manip-digit| | |tracking-loco-manip-digit-link| | Track a root velocity and hand pose command with the Agility Digit robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ -.. |velocity-flat-anymal-b-link| replace:: `Isaac-Velocity-Flat-Anymal-B-v0 `__ -.. |velocity-rough-anymal-b-link| replace:: `Isaac-Velocity-Rough-Anymal-B-v0 `__ +.. |velocity-flat-anymal-b-link| replace:: `Isaac-Velocity-Flat-Anymal-B-v0 `__ +.. |velocity-rough-anymal-b-link| replace:: `Isaac-Velocity-Rough-Anymal-B-v0 `__ -.. |velocity-flat-anymal-c-link| replace:: `Isaac-Velocity-Flat-Anymal-C-v0 `__ -.. |velocity-rough-anymal-c-link| replace:: `Isaac-Velocity-Rough-Anymal-C-v0 `__ +.. |velocity-flat-anymal-c-link| replace:: `Isaac-Velocity-Flat-Anymal-C-v0 `__ +.. |velocity-rough-anymal-c-link| replace:: `Isaac-Velocity-Rough-Anymal-C-v0 `__ -.. |velocity-flat-anymal-c-direct-link| replace:: `Isaac-Velocity-Flat-Anymal-C-Direct-v0 `__ -.. |velocity-rough-anymal-c-direct-link| replace:: `Isaac-Velocity-Rough-Anymal-C-Direct-v0 `__ +.. |velocity-flat-anymal-c-direct-link| replace:: `Isaac-Velocity-Flat-Anymal-C-Direct-v0 `__ +.. |velocity-rough-anymal-c-direct-link| replace:: `Isaac-Velocity-Rough-Anymal-C-Direct-v0 `__ -.. |velocity-flat-anymal-d-link| replace:: `Isaac-Velocity-Flat-Anymal-D-v0 `__ -.. |velocity-rough-anymal-d-link| replace:: `Isaac-Velocity-Rough-Anymal-D-v0 `__ +.. |velocity-flat-anymal-d-link| replace:: `Isaac-Velocity-Flat-Anymal-D-v0 `__ +.. |velocity-rough-anymal-d-link| replace:: `Isaac-Velocity-Rough-Anymal-D-v0 `__ -.. |velocity-flat-unitree-a1-link| replace:: `Isaac-Velocity-Flat-Unitree-A1-v0 `__ -.. |velocity-rough-unitree-a1-link| replace:: `Isaac-Velocity-Rough-Unitree-A1-v0 `__ +.. |velocity-flat-unitree-a1-link| replace:: `Isaac-Velocity-Flat-Unitree-A1-v0 `__ +.. |velocity-rough-unitree-a1-link| replace:: `Isaac-Velocity-Rough-Unitree-A1-v0 `__ -.. |velocity-flat-unitree-go1-link| replace:: `Isaac-Velocity-Flat-Unitree-Go1-v0 `__ -.. |velocity-rough-unitree-go1-link| replace:: `Isaac-Velocity-Rough-Unitree-Go1-v0 `__ +.. |velocity-flat-unitree-go1-link| replace:: `Isaac-Velocity-Flat-Unitree-Go1-v0 `__ +.. |velocity-rough-unitree-go1-link| replace:: `Isaac-Velocity-Rough-Unitree-Go1-v0 `__ -.. |velocity-flat-unitree-go2-link| replace:: `Isaac-Velocity-Flat-Unitree-Go2-v0 `__ -.. |velocity-rough-unitree-go2-link| replace:: `Isaac-Velocity-Rough-Unitree-Go2-v0 `__ +.. |velocity-flat-unitree-go2-link| replace:: `Isaac-Velocity-Flat-Unitree-Go2-v0 `__ +.. |velocity-rough-unitree-go2-link| replace:: `Isaac-Velocity-Rough-Unitree-Go2-v0 `__ -.. |velocity-flat-spot-link| replace:: `Isaac-Velocity-Flat-Spot-v0 `__ +.. |velocity-flat-spot-link| replace:: `Isaac-Velocity-Flat-Spot-v0 `__ -.. |velocity-flat-h1-link| replace:: `Isaac-Velocity-Flat-H1-v0 `__ -.. |velocity-rough-h1-link| replace:: `Isaac-Velocity-Rough-H1-v0 `__ +.. |velocity-flat-h1-link| replace:: `Isaac-Velocity-Flat-H1-v0 `__ +.. |velocity-rough-h1-link| replace:: `Isaac-Velocity-Rough-H1-v0 `__ -.. |velocity-flat-g1-link| replace:: `Isaac-Velocity-Flat-G1-v0 `__ -.. |velocity-rough-g1-link| replace:: `Isaac-Velocity-Rough-G1-v0 `__ +.. |velocity-flat-g1-link| replace:: `Isaac-Velocity-Flat-G1-v0 `__ +.. |velocity-rough-g1-link| replace:: `Isaac-Velocity-Rough-G1-v0 `__ +.. |velocity-flat-digit-link| replace:: `Isaac-Velocity-Flat-Digit-v0 `__ +.. |velocity-rough-digit-link| replace:: `Isaac-Velocity-Rough-Digit-v0 `__ +.. |tracking-loco-manip-digit-link| replace:: `Isaac-Tracking-LocoManip-Digit-v0 `__ .. |velocity-flat-anymal-b| image:: ../_static/tasks/locomotion/anymal_b_flat.jpg .. |velocity-rough-anymal-b| image:: ../_static/tasks/locomotion/anymal_b_rough.jpg @@ -282,6 +442,9 @@ Environments based on legged locomotion tasks. .. |velocity-rough-h1| image:: ../_static/tasks/locomotion/h1_rough.jpg .. |velocity-flat-g1| image:: ../_static/tasks/locomotion/g1_flat.jpg .. |velocity-rough-g1| image:: ../_static/tasks/locomotion/g1_rough.jpg +.. |velocity-flat-digit| image:: ../_static/tasks/locomotion/agility_digit_flat.jpg +.. |velocity-rough-digit| image:: ../_static/tasks/locomotion/agility_digit_rough.jpg +.. |tracking-loco-manip-digit| image:: ../_static/tasks/locomotion/agility_digit_loco_manip.jpg Navigation ~~~~~~~~~~ @@ -295,7 +458,7 @@ Navigation | |anymal_c_nav| | |anymal_c_nav-link| | Navigate towards a target x-y position and heading with the ANYmal C robot. | +----------------+---------------------+-----------------------------------------------------------------------------+ -.. |anymal_c_nav-link| replace:: `Isaac-Navigation-Flat-Anymal-C-v0 `__ +.. |anymal_c_nav-link| replace:: `Isaac-Navigation-Flat-Anymal-C-v0 `__ .. |anymal_c_nav| image:: ../_static/tasks/navigation/anymal_c_nav.jpg @@ -327,14 +490,129 @@ Others | | |humanoid_amp_walk-link| | | +----------------+---------------------------+-----------------------------------------------------------------------------+ -.. |quadcopter-link| replace:: `Isaac-Quadcopter-Direct-v0 `__ -.. |humanoid_amp_dance-link| replace:: `Isaac-Humanoid-AMP-Dance-Direct-v0 `__ -.. |humanoid_amp_run-link| replace:: `Isaac-Humanoid-AMP-Run-Direct-v0 `__ -.. |humanoid_amp_walk-link| replace:: `Isaac-Humanoid-AMP-Walk-Direct-v0 `__ +.. |quadcopter-link| replace:: `Isaac-Quadcopter-Direct-v0 `__ +.. |humanoid_amp_dance-link| replace:: `Isaac-Humanoid-AMP-Dance-Direct-v0 `__ +.. |humanoid_amp_run-link| replace:: `Isaac-Humanoid-AMP-Run-Direct-v0 `__ +.. |humanoid_amp_walk-link| replace:: `Isaac-Humanoid-AMP-Walk-Direct-v0 `__ .. |quadcopter| image:: ../_static/tasks/others/quadcopter.jpg .. |humanoid_amp| image:: ../_static/tasks/others/humanoid_amp.jpg +Spaces showcase +~~~~~~~~~~~~~~~ + +The |cartpole_showcase| folder contains showcase tasks (based on the *Cartpole* and *Cartpole-Camera* Direct tasks) +for the definition/use of the various Gymnasium observation and action spaces supported in UW Lab. + +.. |cartpole_showcase| replace:: `cartpole_showcase `__ + +.. note:: + + Currently, only UW Lab's Direct workflow supports the definition of observation and action spaces other than ``Box``. + See Direct workflow's :py:obj:`~uwlab.envs.DirectRLEnvCfg.observation_space` / :py:obj:`~uwlab.envs.DirectRLEnvCfg.action_space` + documentation for more details. + +The following tables summarize the different pairs of showcased spaces for the *Cartpole* and *Cartpole-Camera* tasks. +Replace ```` and ```` with the observation and action spaces to be explored in the task names for training and evaluation. + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Showcase spaces for the Cartpole task

+

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

+
action space
 Box Discrete MultiDiscrete

observation

space

 Boxxxx
 Discretexxx
 MultiDiscretexxx
 Dictxxx
 Tuplexxx
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Showcase spaces for the Cartpole-Camera task

+

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

+
action space
 Box Discrete MultiDiscrete

observation

space

 Boxxxx
 Discrete---
 MultiDiscrete---
 Dictxxx
 Tuplexxx
Multi-agent ------------ @@ -360,7 +638,7 @@ Classic .. |cart-double-pendulum| image:: ../_static/tasks/classic/cart_double_pendulum.jpg -.. |cart-double-pendulum-direct-link| replace:: `Isaac-Cart-Double-Pendulum-Direct-v0 `__ +.. |cart-double-pendulum-direct-link| replace:: `Isaac-Cart-Double-Pendulum-Direct-v0 `__ Manipulation ~~~~~~~~~~~~ @@ -378,13 +656,17 @@ Environments based on fixed-arm manipulation tasks. .. |shadow-hand-over| image:: ../_static/tasks/manipulation/shadow_hand_over.jpg -.. |shadow-hand-over-direct-link| replace:: `Isaac-Shadow-Hand-Over-Direct-v0 `__ +.. |shadow-hand-over-direct-link| replace:: `Isaac-Shadow-Hand-Over-Direct-v0 `__ | Comprehensive List of Environments ================================== +For environments that have a different task name listed under ``Inference Task Name``, please use the Inference Task Name +provided when running ``play.py`` or any inferencing workflows. These tasks provide more suitable configurations for +inferencing, including reading from an already trained checkpoint and disabling runtime perturbations used for training. + .. list-table:: :widths: 33 25 19 25 @@ -404,11 +686,47 @@ Comprehensive List of Environments - - Direct - **rl_games** (PPO), **skrl** (IPPO, PPO, MAPPO) - * - Isaac-Cartpole-Depth-Camera-Direct-v0 + * - Isaac-Cartpole-Camera-Showcase-Box-Box-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Box-Discrete-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Box-MultiDiscrete-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Dict-Box-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Dict-Discrete-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Dict-MultiDiscrete-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Tuple-Box-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Tuple-Discrete-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Camera-Showcase-Tuple-MultiDiscrete-Direct-v0 (Requires running with ``--enable_cameras``) + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Depth-Camera-Direct-v0 (Requires running with ``--enable_cameras``) - - Direct - **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Cartpole-Depth-v0 + * - Isaac-Cartpole-Depth-v0 (Requires running with ``--enable_cameras``) - - Manager Based - **rl_games** (PPO) @@ -416,22 +734,82 @@ Comprehensive List of Environments - - Direct - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) - * - Isaac-Cartpole-RGB-Camera-Direct-v0 + * - Isaac-Cartpole-RGB-Camera-Direct-v0 (Requires running with ``--enable_cameras``) - - Direct - **rl_games** (PPO), **skrl** (PPO) - * - Isaac-Cartpole-RGB-ResNet18-v0 + * - Isaac-Cartpole-RGB-ResNet18-v0 (Requires running with ``--enable_cameras``) - - Manager Based - **rl_games** (PPO) - * - Isaac-Cartpole-RGB-TheiaTiny-v0 + * - Isaac-Cartpole-RGB-TheiaTiny-v0 (Requires running with ``--enable_cameras``) - - Manager Based - **rl_games** (PPO) - * - Isaac-Cartpole-RGB-v0 + * - Isaac-Cartpole-RGB-v0 (Requires running with ``--enable_cameras``) - - Manager Based - **rl_games** (PPO) + * - Isaac-Cartpole-Showcase-Box-Box-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Box-Discrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Box-MultiDiscrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Dict-Box-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Dict-Discrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Dict-MultiDiscrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Discrete-Box-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Discrete-Discrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Discrete-MultiDiscrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-MultiDiscrete-Box-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-MultiDiscrete-Discrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-MultiDiscrete-MultiDiscrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Tuple-Box-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Tuple-Discrete-Direct-v0 + - + - Direct + - **skrl** (PPO) + * - Isaac-Cartpole-Showcase-Tuple-MultiDiscrete-Direct-v0 + - + - Direct + - **skrl** (PPO) * - Isaac-Cartpole-v0 - - Manager Based @@ -448,6 +826,26 @@ Comprehensive List of Environments - - Direct - **rl_games** (PPO) + * - Isaac-AutoMate-Assembly-Direct-v0 + - + - Direct + - **rl_games** (PPO) + * - Isaac-AutoMate-Disassembly-Direct-v0 + - + - Direct + - + * - Isaac-Forge-GearMesh-Direct-v0 + - + - Direct + - **rl_games** (PPO) + * - Isaac-Forge-NutThread-Direct-v0 + - + - Direct + - **rl_games** (PPO) + * - Isaac-Forge-PegInsert-Direct-v0 + - + - Direct + - **rl_games** (PPO) * - Isaac-Franka-Cabinet-Direct-v0 - - Direct @@ -488,6 +886,10 @@ Comprehensive List of Environments - - Manager Based - + * - Isaac-Tracking-LocoManip-Digit-v0 + - Isaac-Tracking-LocoManip-Digit-Play-v0 + - Manager Based + - **rsl_rl** (PPO) * - Isaac-Navigation-Flat-Anymal-C-v0 - Isaac-Navigation-Flat-Anymal-C-Play-v0 - Manager Based @@ -528,6 +930,10 @@ Comprehensive List of Environments - Isaac-Reach-UR10-Play-v0 - Manager Based - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Deploy-Reach-UR10e-v0 + - Isaac-Deploy-Reach-UR10e-Play-v0 + - Manager Based + - **rsl_rl** (PPO) * - Isaac-Repose-Cube-Allegro-Direct-v0 - - Direct @@ -552,8 +958,8 @@ Comprehensive List of Environments - - Direct - **rl_games** (LSTM) - * - Isaac-Repose-Cube-Shadow-Vision-Direct-v0 - - Isaac-Repose-Cube-Shadow-Vision-Direct-Play-v0 + * - Isaac-Repose-Cube-Shadow-Vision-Direct-v0 (Requires running with ``--enable_cameras``) + - Isaac-Repose-Cube-Shadow-Vision-Direct-Play-v0 (Requires running with ``--enable_cameras``) - Direct - **rsl_rl** (PPO), **rl_games** (VISION) * - Isaac-Shadow-Hand-Over-Direct-v0 @@ -564,6 +970,14 @@ Comprehensive List of Environments - - Manager Based - + * - Isaac-Dexsuite-Kuka-Allegro-Lift-v0 + - Isaac-Dexsuite-Kuka-Allegro-Lift-Play-v0 + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO) + * - Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 + - Isaac-Dexsuite-Kuka-Allegro-Reorient-Play-v0 + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO) * - Isaac-Stack-Cube-Franka-v0 - - Manager Based @@ -576,6 +990,59 @@ Comprehensive List of Environments - - Manager Based - + * - Isaac-PickPlace-G1-InspireFTP-Abs-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-UR10-Long-Suction-IK-Rel-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-v0 + - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-Play-v0 + - Manager Based + - + * - Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-v0 + - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-Play-v0 + - Manager Based + - + * - Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Velocity-Flat-Anymal-B-v0 - Isaac-Velocity-Flat-Anymal-B-Play-v0 - Manager Based @@ -596,6 +1063,10 @@ Comprehensive List of Environments - Isaac-Velocity-Flat-Cassie-Play-v0 - Manager Based - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Digit-v0 + - Isaac-Velocity-Flat-Digit-Play-v0 + - Manager Based + - **rsl_rl** (PPO) * - Isaac-Velocity-Flat-G1-v0 - Isaac-Velocity-Flat-G1-Play-v0 - Manager Based @@ -611,7 +1082,7 @@ Comprehensive List of Environments * - Isaac-Velocity-Flat-Unitree-A1-v0 - Isaac-Velocity-Flat-Unitree-A1-Play-v0 - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) + - **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) * - Isaac-Velocity-Flat-Unitree-Go1-v0 - Isaac-Velocity-Flat-Unitree-Go1-Play-v0 - Manager Based @@ -640,6 +1111,10 @@ Comprehensive List of Environments - Isaac-Velocity-Rough-Cassie-Play-v0 - Manager Based - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Digit-v0 + - Isaac-Velocity-Rough-Digit-Play-v0 + - Manager Based + - **rsl_rl** (PPO) * - Isaac-Velocity-Rough-G1-v0 - Isaac-Velocity-Rough-G1-Play-v0 - Manager Based @@ -651,7 +1126,7 @@ Comprehensive List of Environments * - Isaac-Velocity-Rough-Unitree-A1-v0 - Isaac-Velocity-Rough-Unitree-A1-Play-v0 - Manager Based - - **rsl_rl** (PPO), **skrl** (PPO) + - **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) * - Isaac-Velocity-Rough-Unitree-Go1-v0 - Isaac-Velocity-Rough-Unitree-Go1-Play-v0 - Manager Based diff --git a/docs/source/overview/uw_environments.rst b/docs/source/overview/uw_environments.rst index 3108f54..ba24670 100644 --- a/docs/source/overview/uw_environments.rst +++ b/docs/source/overview/uw_environments.rst @@ -1,4 +1,4 @@ -.. _environments: +.. _uw_environments: Available UW Environments =========================== @@ -51,6 +51,12 @@ Environments based on fixed-arm manipulation tasks. +--------------------------------+------------------------------------------------+------------------------------------------------------------------------------+ | |ext-peg-insert-franka| | |ext-peg-insert-franka-link| | Inserting peg rod into hole on nist board | +--------------------------------+------------------------------------------------+------------------------------------------------------------------------------+ + | |omnireset-ur5e-drawer| | |omnireset-ur5e-drawer-link| | Assemble drawer into the bottom of cabinet | + +--------------------------------+------------------------------------------------+------------------------------------------------------------------------------+ + | |omnireset-ur5e-fbleg| | |omnireset-ur5e-fbleg-link| | Insert and twist leg into a furniture bench table top | + +--------------------------------+------------------------------------------------+------------------------------------------------------------------------------+ + | |omnireset-ur5e-peg-insert| | |omnireset-ur5e-peg-insert-link| | Insert a square peg into the square peg hole | + +--------------------------------+------------------------------------------------+------------------------------------------------------------------------------+ .. |track-goal-ur5| image:: ../_static/tasks/manipulation/ur5_track_goal.jpg .. |track-goal-tycho| image:: ../_static/tasks/manipulation/tycho_track_goal.jpg @@ -58,13 +64,20 @@ Environments based on fixed-arm manipulation tasks. .. |ext-nut-thread-franka| image:: ../_static/tasks/manipulation/factory_ext/nut_thread_ext.jpg .. |ext-gear-mesh-franka| image:: ../_static/tasks/manipulation/factory_ext/gear_mesh_ext.jpg .. |ext-peg-insert-franka| image:: ../_static/tasks/manipulation/factory_ext/peg_insert_ext.jpg +.. |omnireset-ur5e-drawer| image:: ../_static/tasks/manipulation/omnireset_drawer_assemble.jpg +.. |omnireset-ur5e-fbleg| image:: ../_static/tasks/manipulation/omnireset_fbleg_screw.jpg +.. |omnireset-ur5e-peg-insert| image:: ../_static/tasks/manipulation/omnireset_peg_insert.jpg + +.. |track-goal-ur5-link| replace:: `UW-Track-Goal-Ur5-v0 `__ +.. |track-goal-tycho-link| replace:: `UW-Track-Goal-Tycho-v0 `__ +.. |track-goal-xarm-leap-link| replace:: `UW-Track-Goal-Xarm-Leap-v0 `__ +.. |ext-nut-thread-franka-link| replace:: `UW-Nut-Thread-Franka-v0 `__ +.. |ext-gear-mesh-franka-link| replace:: `UW-Gear-Mesh-Franka-v0 `__ +.. |ext-peg-insert-franka-link| replace:: `UW-Peg-Insert-Franka-v0 `__ +.. |omnireset-ur5e-drawer-link| replace:: `OmniReset-Ur5eRobotiq2f85-RelJointPos-State-v0 `__ +.. |omnireset-ur5e-fbleg-link| replace:: `OmniReset-Ur5eRobotiq2f85-RelJointPos-State-v0 `__ +.. |omnireset-ur5e-peg-insert-link| replace:: `OmniReset-Ur5eRobotiq2f85-RelJointPos-State-v0 `__ -.. |track-goal-ur5-link| replace:: `UW-Track-Goal-Ur5-v0 `__ -.. |track-goal-tycho-link| replace:: `UW-Track-Goal-Tycho-v0 `__ -.. |track-goal-xarm-leap-link| replace:: `UW-Track-Goal-Xarm-Leap-v0 `__ -.. |ext-nut-thread-franka-link| replace:: `UW-Nut-Thread-Franka-v0 `__ -.. |ext-gear-mesh-franka-link| replace:: `UW-Gear-Mesh-Franka-v0 `__ -.. |ext-peg-insert-franka-link| replace:: `UW-Peg-Insert-Franka-v0 `__ Locomotion ~~~~~~~~~~ @@ -88,11 +101,11 @@ Environments based on legged locomotion tasks. | |position-obstacle-spot| | |position-obstacle-spot-link| | Track a position command on obstacle terrain with the Spot robot | +--------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ -.. |position-gap-spot-link| replace:: `UW-Position-Gap-Spot-v0 `__ -.. |position-pit-spot-link| replace:: `UW-Position-Pit-Spot-v0 `__ -.. |position-stepping-stone-spot-link| replace:: `UW-Position-Stepping-Stone-Spot-v0 `__ -.. |position-obstacle-spot-link| replace:: `UW-Position-Obstacle-Spot-v0 `__ -.. |position-inv-slope-spot-link| replace:: `UW-Position-Inv-Slope-Spot-v0 `__ +.. |position-gap-spot-link| replace:: `UW-Position-Gap-Spot-v0 `__ +.. |position-pit-spot-link| replace:: `UW-Position-Pit-Spot-v0 `__ +.. |position-stepping-stone-spot-link| replace:: `UW-Position-Stepping-Stone-Spot-v0 `__ +.. |position-obstacle-spot-link| replace:: `UW-Position-Obstacle-Spot-v0 `__ +.. |position-inv-slope-spot-link| replace:: `UW-Position-Inv-Slope-Spot-v0 `__ .. |position-gap-spot| image:: ../_static/tasks/locomotion/spot_gap.jpg .. |position-pit-spot| image:: ../_static/tasks/locomotion/spot_pit.jpg @@ -164,3 +177,11 @@ Comprehensive List of Environments - - Manager Based - **rsl_rl** (PPO) + * - OmniReset-Ur5eRobotiq2f85-RelJointPos-State-v0 + - + - Manager Based + - **rsl_rl** (PPO) + * - OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 + - + - Manager Based + - **rsl_rl** (PPO) diff --git a/docs/source/publications/omnireset/index.rst b/docs/source/publications/omnireset/index.rst new file mode 100644 index 0000000..ee43885 --- /dev/null +++ b/docs/source/publications/omnireset/index.rst @@ -0,0 +1,52 @@ +OmniReset +========= + +**OmniReset** is a robotic manipulation framework using RL to solve dexterous, contact-rich manipulation tasks without reward engineering or demos. + +.. important:: + **Pre-trained RL Checkpoints Available!** + + We provide trained RL checkpoints for all three tasks: **Drawer Assembly**, **Leg Twisting**, and **Peg Insertion**. + Download the checkpoints and evaluate them immediately! + + See the :doc:`instruction` guide for download links and evaluation instructions. + +.. raw:: html + +
+
+ +

Drawer Assembly

+
+
+ +

Leg Twisting

+
+
+ +

Peg Insertion

+
+
+ +.. note:: + Detailed documentation will be updated following the public release of the paper. + +Getting Started +--------------- + +For setup, training, and evaluation instructions, see :doc:`instruction`. + +.. toctree:: + :maxdepth: 1 + :hidden: + + instruction diff --git a/docs/source/publications/omnireset/instruction.rst b/docs/source/publications/omnireset/instruction.rst new file mode 100644 index 0000000..53c7d7c --- /dev/null +++ b/docs/source/publications/omnireset/instruction.rst @@ -0,0 +1,199 @@ +Instructions +============ + +This guide provides step-by-step instructions for using the OmniReset framework. +Choose your path: :ref:`evaluate our pre-trained checkpoints ` or :ref:`reproduce training from scratch `. + +.. note:: + + For all commands below, replace ``insertive_object`` and ``receptive_object`` with one of the following: + + * **Drawer Assembly:** ``fbdrawerbottom`` / ``fbdrawerbox`` + * **Twisting:** ``fbleg`` / ``fbtabletop`` + * **Insertion:** ``peg`` / ``peghole`` + + For grasp sampling, replace ``object`` with ``fbleg``, ``fbdrawerbottom``, or ``peg``. + +---- + +.. _evaluate-checkpoints: + +Download and Evaluate Pre-trained Checkpoints +--------------------------------------------- + +We provide trained RL checkpoints for all three tasks. Download and evaluate them immediately! + +Download Checkpoints +^^^^^^^^^^^^^^^^^^^^ + +Download the pre-trained checkpoints from our Backblaze B2 storage (drawer assembly, leg twisting, peg insertion): + +.. code:: bash + + wget https://s3.us-west-004.backblazeb2.com/uwlab-assets/Policies/OmniReset/fbdrawerbottom_state_rl_expert.pt + wget https://s3.us-west-004.backblazeb2.com/uwlab-assets/Policies/OmniReset/fbleg_state_rl_expert.pt + wget https://s3.us-west-004.backblazeb2.com/uwlab-assets/Policies/OmniReset/peg_state_rl_expert.pt + +Evaluate Checkpoints +^^^^^^^^^^^^^^^^^^^^ + +Run evaluation on the downloaded checkpoints: + +.. code:: bash + + python scripts/reinforcement_learning/rsl_rl/play.py --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 --num_envs 1 --checkpoint /path/to/checkpoint.pt env.scene.insertive_object=insertive_object env.scene.receptive_object=receptive_object + + +.. _reproduce-training: + +Reproduce Our Training +---------------------- + +Follow these steps to reproduce our training results from scratch. This involves collecting reset state datasets and training RL policies. + +Collect Partial Assemblies +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Collect partial assembly datasets that will be used for generating reset states. +You can either use existing datasets from Backblaze or collect new ones. + +.. code:: bash + + python scripts_v2/tools/record_partial_assemblies.py --task OmniReset-PartialAssemblies-v0 --num_envs 10 --num_trajectories 10 --dataset_dir ./partial_assembly_datasets --headless env.scene.insertive_object=insertive_object env.scene.receptive_object=receptive_object + +.. note:: + + This step should take approximately 30 seconds. + + +Sample Grasp Poses +^^^^^^^^^^^^^^^^^^ + +Sample grasp poses for the objects. You can either use existing datasets from Backblaze or collect new ones. + +.. code:: bash + + python scripts_v2/tools/record_grasps.py --task OmniReset-Robotiq2f85-GraspSampling-v0 --num_envs 8192 --num_grasps 1000 --dataset_dir ./grasp_datasets --headless env.scene.object=object + +.. note:: + + This step should take approximately 1 minute. + + +Generate Reset State Datasets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Generate reset state datasets for different configurations. You can either use existing datasets from Backblaze or collect new ones. + +.. important:: + + Before running these scripts, make sure ``base_path`` and ``base_paths`` in ``reset_states_cfg.py`` are set appropriately. + +Object Anywhere, End-Effector Anywhere (Reaching) +""""""""""""""""""""""""""""""""""""""""""""""""" + +.. code:: bash + + python scripts_v2/tools/record_reset_states.py --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0 --num_envs 4096 --num_reset_states 10000 --headless --dataset_dir ./reset_state_datasets/ObjectAnywhereEEAnywhere env.scene.insertive_object=insertive_object env.scene.receptive_object=receptive_object + +Object Resting, End-Effector Grasped (Near Object) +"""""""""""""""""""""""""""""""""""""""""""""""""" + +.. code:: bash + + python scripts_v2/tools/record_reset_states.py --task OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0 --num_envs 4096 --num_reset_states 10000 --headless --dataset_dir ./reset_state_datasets/ObjectRestingEEGrasped env.scene.insertive_object=insertive_object env.scene.receptive_object=receptive_object + +Object Anywhere, End-Effector Grasped (Grasped) +""""""""""""""""""""""""""""""""""""""""""""""" + +.. code:: bash + + python scripts_v2/tools/record_reset_states.py --task OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0 --num_envs 4096 --num_reset_states 10000 --headless --dataset_dir ./reset_state_datasets/ObjectAnywhereEEGrasped env.scene.insertive_object=insertive_object env.scene.receptive_object=receptive_object + +Object Partially Assembled, End-Effector Grasped (Near Goal) +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +.. code:: bash + + python scripts_v2/tools/record_reset_states.py --task OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0 --num_envs 4096 --num_reset_states 10000 --headless --dataset_dir ./reset_state_datasets/ObjectPartiallyAssembledEEGrasped env.scene.insertive_object=insertive_object env.scene.receptive_object=receptive_object + +.. note:: + + Each of these steps should take anywhere between 1 minute and 1 hour depending on the task and reset configuration. + + +Visualize Reset States (Optional) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Visualize the generated reset states to verify they are correct. + +.. code:: bash + + python scripts_v2/tools/visualize_reset_states.py --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0 --num_envs 4 --dataset_dir /path/to/dataset env.scene.insertive_object=insertive_object env.scene.receptive_object=receptive_object + + +Train RL Policy +^^^^^^^^^^^^^^^ + +Train reinforcement learning policies using the generated reset states. + +.. code:: bash + + python -m torch.distributed.run \ + --nnodes 1 \ + --nproc_per_node 4 \ + scripts/reinforcement_learning/rsl_rl/train.py \ + --task OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0 \ + --num_envs 16384 \ + --logger wandb \ + --headless \ + --distributed \ + env.scene.insertive_object=insertive_object \ + env.scene.receptive_object=receptive_object + +Training Curves +^^^^^^^^^^^^^^^ + +Below are sample training curves for each task: + +.. list-table:: + :widths: 33 33 33 + :class: borderless + + * - .. figure:: ../../../source/_static/publications/omnireset/drawer_assembly_training_curve.jpg + :width: 100% + :alt: Drawer assembly training curve + + Drawer Assembly task + + - .. figure:: ../../../source/_static/publications/omnireset/twisting_training_curve.jpg + :width: 100% + :alt: Leg Twisting training curve + + Leg Twisting task + + - .. figure:: ../../../source/_static/publications/omnireset/peg_training_curve.jpg + :width: 100% + :alt: Peg Insertion training curve + + Peg Insertion task + +---- + +Known Issues and Solutions +-------------------------- + +GLIBCXX Version Error +^^^^^^^^^^^^^^^^^^^^^ + +If you encounter this error: + +.. code-block:: text + + OSError: version `GLIBCXX_3.4.30' not found (required by /path/to/omni/libcarb.so) + +Try exporting the system's ``libstdc++`` library: + +.. code:: bash + + export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 diff --git a/docs/source/publications/pg1.rst b/docs/source/publications/pg1.rst index c09dec9..a109ad7 100644 --- a/docs/source/publications/pg1.rst +++ b/docs/source/publications/pg1.rst @@ -4,7 +4,7 @@ Parental Guidance(PG1) Links ----- - **Paper on OpenReview:** `Parental Guidance: Efficient Lifelong Learning through Evolutionary Distillation `_ -- **GitHub Repository:** `UW Lab GitHub `_ +- **GitHub Repository:** `UW Lab GitHub `_ Authors ------- diff --git a/docs/source/refs/license.rst b/docs/source/refs/license.rst index 0eb154a..bfbba6f 100644 --- a/docs/source/refs/license.rst +++ b/docs/source/refs/license.rst @@ -13,9 +13,8 @@ The license files for all its dependencies and included assets are available in The Isaac Lab framework is open-sourced under the `BSD-3-Clause license `_. - The UW Lab framework is open-sourced under the -`BSD-3-Clause license `_. +`BSD-3-Clause license `_, with some dependencies licensed under other terms. .. code-block:: text @@ -49,3 +48,4 @@ The UW Lab framework is open-sourced under the ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + Β© [2025] [UWLab]. All Rights Reserved. diff --git a/docs/source/setup/installation/asset_caching.rst b/docs/source/setup/installation/asset_caching.rst new file mode 100644 index 0000000..f8b9831 --- /dev/null +++ b/docs/source/setup/installation/asset_caching.rst @@ -0,0 +1,58 @@ +Asset Caching +============= + +Assets used in UW Lab are hosted on AWS S3 buckets on the cloud. +Asset loading time can depend on your network connection and geographical location. +In some cases, it is possible that asset loading times can be long when assets are pulled from the AWS servers. + +If you run into cases where assets take a few minutes to load for each run, +we recommend enabling asset caching following the below steps. + +First, launch the Isaac Sim application: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./uwlab.sh -s + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + uwlab.bat -s + +On the top right of the UW Lab or Isaac Sim app, look for the icon labeled ``CACHE:``. +You may see a message such as ``HUB NOT DETECTED`` or ``NEW VERSION DETECTED``. + +Click the message to enable `Hub `_. +Hub automatically manages local caching for UW Lab assets, so subsequent runs will use cached files instead of +downloading from AWS each time. + +.. figure:: /source/_static/setup/asset_caching.jpg + :align: center + :figwidth: 100% + :alt: Simulator with cache messaging. + +Hub provides better control and management of cached assets, making workflows faster and more reliable, especially +in environments with limited or intermittent internet access. + +.. note:: + The first time you run UW Lab, assets will still need to be pulled from the cloud, which could lead + to longer loading times. Once cached, loading times will be significantly reduced on subsequent runs. + +Nucleus +------- + + +Before Isaac Sim 4.5, assets were accessed via the Omniverse Nucleus server, including setups with local Nucleus instances. + +.. warning:: + Starting with Isaac Sim 4.5, the Omniverse Nucleus server and Omniverse Launcher are deprecated. + Existing Nucleus setups will continue to work, so if you have a local Nucleus server already configured, + you may continue to use it. diff --git a/docs/source/setup/installation/binaries_installation.rst b/docs/source/setup/installation/binaries_installation.rst new file mode 100644 index 0000000..0dc833b --- /dev/null +++ b/docs/source/setup/installation/binaries_installation.rst @@ -0,0 +1,80 @@ +.. _uwlab-binaries-installation: + +Installation using Isaac Sim Pre-built Binaries +=============================================== + +The following steps first installs Isaac Sim from its pre-built binaries, then UW Lab from source code. + +Installing Isaac Sim +-------------------- + +Downloading pre-built binaries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Isaac Sim binaries can be downloaded directly as a zip file from +`here `__. +If you wish to use the older Isaac Sim 4.5 release, please check the older download page +`here `__. + +Once the zip file is downloaded, you can unzip it to the desired directory. +As an example set of instructions for unzipping the Isaac Sim binaries, +please refer to the `Isaac Sim documentation `__. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + On Linux systems, we assume the Isaac Sim directory is named ``${HOME}/isaacsim``. + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + On Windows systems, we assume the Isaac Sim directory is named ``C:\isaacsim``. + +Verifying the Isaac Sim installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To avoid the overhead of finding and locating the Isaac Sim installation +directory every time, we recommend exporting the following environment +variables to your terminal for the remaining of the installation instructions: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Isaac Sim root directory + export ISAACSIM_PATH="${HOME}/isaacsim" + # Isaac Sim python executable + export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Isaac Sim root directory + set ISAACSIM_PATH="C:\isaacsim" + :: Isaac Sim python executable + set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat" + + +.. include:: include/bin_verify_isaacsim.rst + +Installing UW Lab +-------------------- + +.. include:: include/src_clone_uwlab.rst + +.. include:: include/src_symlink_isaacsim.rst + +.. include:: include/src_python_virtual_env.rst + +.. include:: include/src_build_uwlab.rst + +.. include:: include/src_verify_uwlab.rst diff --git a/docs/source/setup/installation/include/bin_verify_isaacsim.rst b/docs/source/setup/installation/include/bin_verify_isaacsim.rst new file mode 100644 index 0000000..19da95e --- /dev/null +++ b/docs/source/setup/installation/include/bin_verify_isaacsim.rst @@ -0,0 +1,74 @@ +Check that the simulator runs as expected: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # note: you can pass the argument "--help" to see all arguments possible. + ${ISAACSIM_PATH}/isaac-sim.sh + + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: note: you can pass the argument "--help" to see all arguments possible. + %ISAACSIM_PATH%\isaac-sim.bat + + +Check that the simulator runs from a standalone python script: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # checks that python path is set correctly + ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')" + # checks that Isaac Sim can be launched from python + ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.api/add_cubes.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: checks that python path is set correctly + %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')" + :: checks that Isaac Sim can be launched from python + %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\isaacsim.core.api\add_cubes.py + +.. caution:: + + If you have been using a previous version of Isaac Sim, you need to run the following command for the *first* + time after installation to remove all the old user data and cached variables: + + .. tab-set:: + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + + .. code:: bash + + ${ISAACSIM_PATH}/isaac-sim.sh --reset-user + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + + .. code:: batch + + %ISAACSIM_PATH%\isaac-sim.bat --reset-user + + +If the simulator does not run or crashes while following the above +instructions, it means that something is incorrectly configured. To +debug and troubleshoot, please check Isaac Sim +`documentation `__ +and the +`Isaac Sim Forums `_. diff --git a/docs/source/setup/installation/include/pip_python_virtual_env.rst b/docs/source/setup/installation/include/pip_python_virtual_env.rst new file mode 100644 index 0000000..4f6c87e --- /dev/null +++ b/docs/source/setup/installation/include/pip_python_virtual_env.rst @@ -0,0 +1,123 @@ +Preparing a Python Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating a dedicated Python environment is **strongly recommended**. It helps: + +- **Avoid conflicts with system Python** or other projects installed on your machine. +- **Keep dependencies isolated**, so that package upgrades or experiments in other projects + do not break Isaac Sim. +- **Easily manage multiple environments** for setups with different versions of dependencies. +- **Simplify reproducibility** β€” the environment contains only the packages needed for the current project, + making it easier to share setups with colleagues or run on different machines. + +You can choose different package managers to create a virtual environment. + +- **UV**: A modern, fast, and secure package manager for Python. +- **Conda**: A cross-platform, language-agnostic package manager for Python. +- **venv**: The standard library for creating virtual environments in Python. + +.. caution:: + + The Python version of the virtual environment must match the Python version of Isaac Sim. + + - For Isaac Sim 5.X, the required Python version is 3.11. + - For Isaac Sim 4.X, the required Python version is 3.10. + + Using a different Python version will result in errors when running UW Lab. + +The following instructions are for Isaac Sim 5.X, which requires Python 3.11. +If you wish to install Isaac Sim 4.5, please use modify the instructions accordingly to use Python 3.10. + +- Create a virtual environment using one of the package managers: + + .. tab-set:: + + .. tab-item:: UV Environment + + To install ``uv``, please follow the instructions `here `__. + You can create the UW Lab environment using the following commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named env_uwlab with python3.11 + uv venv --python 3.11 env_uwlab + # activate the virtual environment + source env_uwlab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + :: create a virtual environment named env_uwlab with python3.11 + uv venv --python 3.11 env_uwlab + :: activate the virtual environment + env_uwlab\Scripts\activate + + .. tab-item:: Conda Environment + + To install conda, please follow the instructions `here `__. + You can create the UW 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_uwlab python=3.11 + conda activate env_uwlab + + .. tab-item:: venv Environment + + To create a virtual environment using the standard library, you can use the + following commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named env_uwlab with python3.11 + python3.11 -m venv env_uwlab + # activate the virtual environment + source env_uwlab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + :: create a virtual environment named env_uwlab with python3.11 + python3.11 -m venv env_uwlab + :: activate the virtual environment + env_uwlab\Scripts\activate + + +- Ensure the latest pip version is installed. To update pip, run the following command + from inside the virtual environment: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + pip install --upgrade pip + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + python -m pip install --upgrade pip diff --git a/docs/source/setup/installation/include/pip_verify_isaacsim.rst b/docs/source/setup/installation/include/pip_verify_isaacsim.rst new file mode 100644 index 0000000..111b47d --- /dev/null +++ b/docs/source/setup/installation/include/pip_verify_isaacsim.rst @@ -0,0 +1,46 @@ + +Verifying the Isaac Sim installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Make sure that your virtual environment is activated (if applicable) + +- Check that the simulator runs as expected: + + .. code:: bash + + # note: you can pass the argument "--help" to see all arguments possible. + isaacsim + +- It's also possible to run with a specific experience file, run: + + .. code:: bash + + # experience files can be absolute path, or relative path searched in isaacsim/apps or omni/apps + isaacsim isaacsim.exp.full.kit + + +.. note:: + + When running Isaac Sim for the first time, all dependent extensions will be pulled from the registry. + This process can take upwards of 10 minutes and is required on the first run of each experience file. + Once the extensions are pulled, consecutive runs using the same experience file will use the cached extensions. + +.. attention:: + + The first run will prompt users to accept the Nvidia Omniverse License Agreement. + To accept the EULA, reply ``Yes`` when prompted with the below message: + + .. code:: bash + + By installing or using Isaac Sim, I agree to the terms of NVIDIA OMNIVERSE LICENSE AGREEMENT (EULA) + in https://docs.isaacsim.omniverse.nvidia.com/latest/common/NVIDIA_Omniverse_License_Agreement.html + + Do you accept the EULA? (Yes/No): Yes + + +If the simulator does not run or crashes while following the above +instructions, it means that something is incorrectly configured. To +debug and troubleshoot, please check Isaac Sim +`documentation `__ +and the +`Isaac Sim Forums `_. diff --git a/docs/source/setup/installation/include/src_build_uwlab.rst b/docs/source/setup/installation/include/src_build_uwlab.rst new file mode 100644 index 0000000..3c9be97 --- /dev/null +++ b/docs/source/setup/installation/include/src_build_uwlab.rst @@ -0,0 +1,56 @@ +Installation +~~~~~~~~~~~~ + +- Install dependencies using ``apt`` (on Linux only): + + .. code:: bash + + # these dependency are needed by robomimic which is not available on Windows + sudo apt install cmake build-essential + +- Run the install command that iterates over all the extensions in ``source`` directory and installs them + using pip (with ``--editable`` flag): + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./uwlab.sh --install # or "./uwlab.sh -i" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + uwlab.bat --install :: or "uwlab.bat -i" + + + By default, the above will install **all** the learning frameworks. These include + ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``. + + If you want to install only a specific framework, you can pass the name of the framework + as an argument. For example, to install only the ``rl_games`` framework, you can run: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./uwlab.sh --install rl_games # or "./uwlab.sh -i rl_games" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + uwlab.bat --install rl_games :: or "uwlab.bat -i rl_games" + + The valid options are ``all``, ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, + and ``none``. If ``none`` is passed, then no learning frameworks will be installed. diff --git a/docs/source/setup/installation/include/src_clone_uwlab.rst b/docs/source/setup/installation/include/src_clone_uwlab.rst new file mode 100644 index 0000000..3a8b00d --- /dev/null +++ b/docs/source/setup/installation/include/src_clone_uwlab.rst @@ -0,0 +1,78 @@ +Cloning UW Lab +~~~~~~~~~~~~~~~~~ + +.. note:: + + We recommend making a `fork `_ of the UW Lab repository to contribute + to the project but this is not mandatory to use the framework. If you + make a fork, please replace ``isaac-sim`` with your username + in the following instructions. + +Clone the UW Lab repository into your project's workspace: + +.. tab-set:: + + .. tab-item:: SSH + + .. code:: bash + + git clone git@github.com:uw-lab/UWLab.git + + .. tab-item:: HTTPS + + .. code:: bash + + git clone https://github.com/uw-lab/UWLab.git + + +We provide a helper executable `uwlab.sh `_ +and `uwlab.bat `_ for Linux and Windows +respectively that provides utilities to manage extensions. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: text + + ./uwlab.sh --help + + usage: uwlab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage UW Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside UW Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. + -t, --test Run all python pytest tests. + -o, --docker Run the docker container helper script (docker/container.sh). + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -n, --new Create a new external project or internal task from template. + -c, --conda [NAME] Create the conda environment for UW Lab. Default name is 'env_uwlab'. + -u, --uv [NAME] Create the uv environment for UW Lab. Default name is 'env_uwlab'. + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: text + + uwlab.bat --help + + usage: uwlab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage UW Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside UW Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. + -t, --test Run all python pytest tests. + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -n, --new Create a new external project or internal task from template. + -c, --conda [NAME] Create the conda environment for UW Lab. Default name is 'env_uwlab'. + -u, --uv [NAME] Create the uv environment for UW Lab. Default name is 'env_uwlab'. diff --git a/docs/source/setup/installation/include/src_python_virtual_env.rst b/docs/source/setup/installation/include/src_python_virtual_env.rst new file mode 100644 index 0000000..2f594c5 --- /dev/null +++ b/docs/source/setup/installation/include/src_python_virtual_env.rst @@ -0,0 +1,112 @@ +Setting up a Python Environment (optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attention:: + This step is optional. If you are using the bundled Python with Isaac Sim, you can skip this step. + +Creating a dedicated Python environment for UW Lab is **strongly recommended**, even though +it is optional. Using a virtual environment helps: + +- **Avoid conflicts with system Python** or other projects installed on your machine. +- **Keep dependencies isolated**, so that package upgrades or experiments in other projects + do not break Isaac Sim. +- **Easily manage multiple environments** for setups with different versions of dependencies. +- **Simplify reproducibility** β€” the environment contains only the packages needed for the current project, + making it easier to share setups with colleagues or run on different machines. + + +You can choose different package managers to create a virtual environment. + +- **UV**: A modern, fast, and secure package manager for Python. +- **Conda**: A cross-platform, language-agnostic package manager for Python. + +Once created, you can use the default Python in the virtual environment (*python* or *python3*) +instead of *./uwlab.sh -p* or *uwlab.bat -p*. + +.. caution:: + + The Python version of the virtual environment must match the Python version of Isaac Sim. + + - For Isaac Sim 5.X, the required Python version is 3.11. + - For Isaac Sim 4.X, the required Python version is 3.10. + + Using a different Python version will result in errors when running UW Lab. + + +.. tab-set:: + + .. tab-item:: UV Environment + + To install ``uv``, please follow the instructions `here `__. + You can create the UW Lab environment using the following commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default environment name 'env_uwlab' + ./uwlab.sh --uv # or "./uwlab.sh -u" + # Option 2: Custom name + ./uwlab.sh --uv my_env # or "./uwlab.sh -u my_env" + + .. code:: bash + + # Activate environment + source ./env_uwlab/bin/activate # or "source ./my_env/bin/activate" + + .. 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. + + .. tab-item:: Conda Environment + + To install conda, please follow the instructions `here `__. + You can create the UW 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 + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default environment name 'env_uwlab' + ./uwlab.sh --conda # or "./uwlab.sh -c" + # Option 2: Custom name + ./uwlab.sh --conda my_env # or "./uwlab.sh -c my_env" + + .. code:: bash + + # Activate environment + conda activate env_uwlab # or "conda activate my_env" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Default environment name 'env_uwlab' + uwlab.bat --conda :: or "uwlab.bat -c" + :: Option 2: Custom name + uwlab.bat --conda my_env :: or "uwlab.bat -c my_env" + + .. code:: batch + + :: Activate environment + conda activate env_uwlab # or "conda activate my_env" + +Once you are in the virtual environment, you do not need to use ``./uwlab.sh -p`` or +``uwlab.bat -p`` to run python scripts. You can use the default python executable in your +environment by running ``python`` or ``python3``. However, for the rest of the documentation, +we will assume that you are using ``./uwlab.sh -p`` or ``uwlab.bat -p`` to run python scripts. diff --git a/docs/source/setup/installation/include/src_symlink_isaacsim.rst b/docs/source/setup/installation/include/src_symlink_isaacsim.rst new file mode 100644 index 0000000..c787db5 --- /dev/null +++ b/docs/source/setup/installation/include/src_symlink_isaacsim.rst @@ -0,0 +1,43 @@ +Creating the Isaac Sim Symbolic Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set up a symbolic link between the installed Isaac Sim root folder +and ``_isaac_sim`` in the UW Lab directory. This makes it convenient +to index the python modules and look for extensions shipped with Isaac Sim. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # enter the cloned repository + cd UWLab + # create a symbolic link + ln -s ${ISAACSIM_PATH} _isaac_sim + + # For example: + # Option 1: If pre-built binaries were installed: + # ln -s ${HOME}/isaacsim _isaac_sim + # + # Option 2: If Isaac Sim was built from source: + # ln -s ${HOME}/IsaacSim/_build/linux-x86_64/release _isaac_sim + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: enter the cloned repository + cd UWLab + :: create a symbolic link - requires launching Command Prompt with Administrator access + mklink /D _isaac_sim %ISAACSIM_PATH% + + :: For example: + :: Option 1: If pre-built binaries were installed: + :: mklink /D _isaac_sim C:\isaacsim + :: + :: Option 2: If Isaac Sim was built from source: + :: mklink /D _isaac_sim C:\IsaacSim\_build\windows-x86_64\release diff --git a/docs/source/setup/installation/include/src_verify_uwlab.rst b/docs/source/setup/installation/include/src_verify_uwlab.rst new file mode 100644 index 0000000..6934d17 --- /dev/null +++ b/docs/source/setup/installation/include/src_verify_uwlab.rst @@ -0,0 +1,95 @@ +Verifying the UW Lab installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To verify that the installation was successful, run the following command from the +top of the repository: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Using the uwlab.sh executable + # note: this works for both the bundled python and the virtual environment + ./uwlab.sh -p scripts/tutorials/00_sim/create_empty.py + + # Option 2: Using python in your virtual environment + python scripts/tutorials/00_sim/create_empty.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Using the uwlab.bat executable + :: note: this works for both the bundled python and the virtual environment + uwlab.bat -p scripts\tutorials\00_sim\create_empty.py + + :: Option 2: Using python in your virtual environment + python scripts\tutorials\00_sim\create_empty.py + + +The above command should launch the simulator and display a window with a black +viewport. You can exit the script by pressing ``Ctrl+C`` on your terminal. +On Windows machines, please terminate the process from Command Prompt using +``Ctrl+Break`` or ``Ctrl+fn+B``. + +.. figure:: /source/_static/setup/verify_install.jpg + :align: center + :figwidth: 100% + :alt: Simulator with a black window. + + +If you see this, then the installation was successful! |:tada:| + +.. note:: + + If you see an error ``ModuleNotFoundError: No module named 'isaacsim'``, please ensure that the virtual + environment is activated and ``source _isaac_sim/setup_conda_env.sh`` has been executed (for uv as well). + + +Train a robot! +~~~~~~~~~~~~~~ + +You can now use UW Lab to train a robot through Reinforcement Learning! The quickest way to use UW Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! +We recommend adding ``--headless`` for faster training. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./uwlab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + uwlab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless + +... Or a robot dog! + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./uwlab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + uwlab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless + diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst new file mode 100644 index 0000000..93f3d9c --- /dev/null +++ b/docs/source/setup/installation/index.rst @@ -0,0 +1,185 @@ +.. _uwlab-installation-root: + +Local Installation +================== + +.. image:: https://img.shields.io/badge/IsaacSim-5.1.0-silver.svg + :target: https://developer.nvidia.com/isaac-sim + :alt: IsaacSim 5.1.0 + +.. image:: https://img.shields.io/badge/python-3.11-blue.svg + :target: https://www.python.org/downloads/release/python-31013/ + :alt: Python 3.11 + +.. image:: https://img.shields.io/badge/platform-linux--64-orange.svg + :target: https://releases.ubuntu.com/22.04/ + :alt: Ubuntu 22.04 + +.. image:: https://img.shields.io/badge/platform-windows--64-orange.svg + :target: https://www.microsoft.com/en-ca/windows/windows-11 + :alt: Windows 11 + + +UW Lab installation is available for Windows and Linux. Since it is built on top of Isaac Sim, +it is required to install Isaac Sim before installing UW Lab. This guide explains the +recommended installation methods for both Isaac Sim and UW Lab. + +.. caution:: + + We have dropped support for Isaac Sim versions 4.2.0 and below. We recommend using the latest + Isaac Sim 5.1.0 release to benefit from the latest features and improvements. + + For more information, please refer to the + `Isaac Sim release notes `__. + + +System Requirements +------------------- + +General Requirements +~~~~~~~~~~~~~~~~~~~~ + +For detailed requirements, please see the +`Isaac Sim system requirements `_. +The basic requirements are: + +- **OS:** Ubuntu 22.04 (Linux x64) or Windows 11 (x64) +- **RAM:** 32 GB or more +- **GPU VRAM:** 16 GB or more (additional VRAM may be required for rendering workflows) + +**Isaac Sim is built against a specific Python version**, making +it essential to use the same Python version when installing UW Lab. +The required Python version is as follows: + +- For Isaac Sim 5.X, the required Python version is 3.11. +- For Isaac Sim 4.X, the required Python version is 3.10. + + +Driver Requirements +~~~~~~~~~~~~~~~~~~~ + +Drivers other than those recommended on `Omniverse Technical Requirements `_ +may work but have not been validated against all Omniverse tests. + +- Use the **latest NVIDIA production branch driver**. +- On Linux, version ``580.65.06`` or later is recommended, especially when upgrading to + **Ubuntu 22.04.5 with kernel 6.8.0-48-generic** or newer. +- On Spark, version ``580.95.05`` is recommended. +- On Windows, version ``580.88`` is recommended. +- If you are using a new GPU or encounter driver issues, install the latest production branch + driver from the `Unix Driver Archive `_ + using the ``.run`` installer. + +DGX Spark: details and limitations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The DGX spark is a standalone machine learning device with aarch64 architecture. As a consequence, some +features of UW Lab are not currently supported on the DGX spark. The most noteworthy is that the architecture *requires* CUDA β‰₯ 13, and thus the cu13 build of PyTorch or newer. +Other notable limitations with respect to UW Lab include... + +#. `SkillGen `_ is not supported out of the box. This + is because cuRobo builds native CUDA/C++ extensions that requires specific tooling and library versions which are not validated for use with DGX spark. + +#. Extended reality teleoperation tools such as `OpenXR `_ is not supported. This is due + to encoding performance limitations that have not yet been fully investigated. + +#. SKRL training with `JAX `_ has not been explicitly validated or tested in UW Lab on the DGX Spark. + JAX provides pre-built CUDA wheels only for Linux on x86_64, so on aarch64 systems (e.g., DGX Spark) it runs on CPU only by default. + GPU support requires building JAX from source, which has not been validated in UW Lab. + +#. Livestream and Hub Workstation Cache are not supported on the DGX spark. + + +Troubleshooting +~~~~~~~~~~~~~~~ + +Please refer to the `Linux Troubleshooting `_ +to resolve installation issues in Linux. + +You can use `Isaac Sim Compatibility Checker `_ +to automatically check if the above requirements are met for running Isaac Sim on your system. + +Quick Start (Recommended) +------------------------- + +For most users, the simplest and fastest way to install UW Lab is by following the +:doc:`pip_installation` guide. + +This method will install Isaac Sim via pip and UW Lab through its source code. +If you are new to UW Lab, start here. + + +Choosing an Installation Method +------------------------------- + +Different workflows require different installation methods. +Use this table to decide: + ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Method | Isaac Sim | UW Lab | Best For | Difficulty | ++===================+==============================+==============================+===========================+============+ +| **Recommended** | |:package:| pip install | |:floppy_disk:| source (git) | Beginners, standard use | Easy | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Binary + Source | |:inbox_tray:| binary | |:floppy_disk:| source (git) | Users preferring binary | Easy | +| | download | | install of Isaac Sim | | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Full Source Build | |:floppy_disk:| source (git) | |:floppy_disk:| source (git) | Developers modifying both | Advanced | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Pip Only | |:package:| pip install | |:package:| pip install | External extensions only | Special | +| | | | (no training/examples) | case | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Docker | |:whale:| Docker | |:floppy_disk:| source (git) | Docker users | Advanced | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ + +Next Steps +---------- + +Once you've reviewed the installation methods, continue with the guide that matches your workflow: + +- |:smiley:| :doc:`pip_installation` + + - Install Isaac Sim via pip and UW Lab from source. + - Best for beginners and most users. + +- :doc:`binaries_installation` + + - Install Isaac Sim from its binary package (website download). + - Install UW Lab from its source code. + - Choose this if you prefer not to use pip for Isaac Sim (for instance, on Ubuntu 20.04). + +- :doc:`source_installation` + + - Build Isaac Sim from source. + - Install UW Lab from its source code. + - Recommended only if you plan to modify Isaac Sim itself. + +- :ref:`container-deployment` + + - Install Isaac Sim and UW Lab in a Docker container. + - Best for users who want to use UW Lab in a containerized environment. + + +Asset Caching +------------- + +UW Lab assets are hosted on **AWS S3 cloud storage**. Loading times can vary +depending on your **network connection** and **geographical location**, and in some cases, +assets may take several minutes to load for each run. To improve performance or support +**offline workflows**, we recommend enabling **asset caching**. + +- Cached assets are stored locally, reducing repeated downloads. +- This is especially useful if you have a slow or intermittent internet connection, + or if your deployment environment is offline. + +Please follow the steps :doc:`asset_caching` to enable asset caching and speed up your workflow. + + +.. toctree:: + :maxdepth: 1 + :hidden: + + pip_installation + binaries_installation + source_installation + + asset_caching diff --git a/docs/source/setup/installation/local_installation.rst b/docs/source/setup/installation/local_installation.rst deleted file mode 100644 index 74d0faa..0000000 --- a/docs/source/setup/installation/local_installation.rst +++ /dev/null @@ -1,67 +0,0 @@ -Installing UW Lab -=================== - -UW Lab builds on top of IsaacLab and IsaacSim. Please follow the below instructions to install UW Lab. - - -.. note:: - - If you use Conda, we recommend using `Miniconda `_. - -Install Isaac Lab and Isaac Sim -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. note:: - Please follow one of three ways to install Isaac Lab and Isaac Sim: - For best experience with vscode development, we recommend using Binary Installation. - For easy and quick installation, we recommend using pip installation. - For advanced users, we recommend using IsaacSim and IsaacLab pip installation. - - `IsaacLab with IsaacSim pip installation `_ - - - `IsaacLab with IsaacSim binary Installation `_ - - - `IsaacSim and IsaacLab pip installation `_ - - -Please also go through **Verifying the Isaac Lab Installation:** section in above links, -If the simulator does not run or crashes while following the above -instructions, it means that something is incorrectly configured. To -debug and troubleshoot, please check Isaac Sim -`documentation `__ -and the -`forums `__. - - -Install UW Lab -~~~~~~~~~~~~~~~~ - -- Make sure that your virtual environment is activated (if applicable) - -- Install UW Lab by cloning the repository and running the installation script - - .. code-block:: bash - - git clone https://github.com/UW-Lab/UWLab.git - -- Pip Install UW Lab in edible mode - - .. code-block:: bash - - cd UWLab - ./uwlab.sh -i - - -Verify UW Lab Installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Try running the following command to verify that UW Lab is installed correctly: - -.. code:: bash - - python scripts/reinforcement_learning/rsl_rl/train.py --task UW-Position-Pit-Spot-v0 --num_envs 1024 - - -Congratulations! You have successfully installed UW Lab. diff --git a/docs/source/setup/installation/pip_installation.rst b/docs/source/setup/installation/pip_installation.rst new file mode 100644 index 0000000..895717e --- /dev/null +++ b/docs/source/setup/installation/pip_installation.rst @@ -0,0 +1,107 @@ +.. _uwlab-pip-installation: + +Installation using Isaac Sim Pip Package +======================================== + +The following steps first installs Isaac Sim from pip, then UW Lab from source code. + +.. attention:: + + Installing Isaac Sim with pip requires GLIBC 2.35+ version compatibility. + To check the GLIBC version on your system, use command ``ldd --version``. + + This may pose compatibility issues with some Linux distributions. For instance, Ubuntu 20.04 LTS + has GLIBC 2.31 by default. If you encounter compatibility issues, we recommend following the + :ref:`Isaac Sim Binaries Installation ` approach. + +.. note:: + + If you plan to :ref:`Set up Visual Studio Code ` later, we recommend following the + :ref:`Isaac Sim Binaries Installation ` approach. + +Installing Isaac Sim +-------------------- + +From Isaac Sim 4.0 onwards, it is possible to install Isaac Sim using pip. +This approach makes it easier to install Isaac Sim without requiring to download the Isaac Sim binaries. +If you encounter any issues, please report them to the +`Isaac Sim Forums `_. + +.. attention:: + + On Windows, it may be necessary to `enable long path support `_ + to avoid installation errors due to OS limitations. + +.. include:: include/pip_python_virtual_env.rst + +Installing dependencies +~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + In case you used UV to create your virtual environment, please replace ``pip`` with ``uv pip`` + in the following commands. + +- Install Isaac Sim pip packages: + + .. code-block:: none + + pip install "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.com + +- Install a CUDA-enabled PyTorch build that matches your system architecture: + + .. tab-set:: + :sync-group: pip-platform + + .. tab-item:: :icon:`fa-brands fa-linux` Linux (x86_64) + :sync: linux-x86_64 + + .. code-block:: bash + + pip install -U torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows (x86_64) + :sync: windows-x86_64 + + .. code-block:: bash + + pip install -U torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 + + .. tab-item:: :icon:`fa-brands fa-linux` Linux (aarch64) + :sync: linux-aarch64 + + .. code-block:: bash + + pip install -U torch==2.9.0 torchvision==0.24.0 --index-url https://download.pytorch.org/whl/cu130 + + .. note:: + + After installing UW Lab on aarch64, you may encounter warnings such as: + + .. code-block:: none + + ERROR: ld.so: object '...torch.libs/libgomp-XXXX.so.1.0.0' cannot be preloaded: ignored. + + This occurs when both the system and PyTorch ``libgomp`` (GNU OpenMP) libraries are preloaded. + Isaac Sim expects the **system** OpenMP runtime, while PyTorch sometimes bundles its own. + + To fix this, unset any existing ``LD_PRELOAD`` and set it to use the system library only: + + .. code-block:: bash + + unset LD_PRELOAD + export LD_PRELOAD="$LD_PRELOAD:/lib/aarch64-linux-gnu/libgomp.so.1" + + This ensures the correct ``libgomp`` library is preloaded for both Isaac Sim and UW Lab, + removing the preload warnings during runtime. + +.. include:: include/pip_verify_isaacsim.rst + +Installing UW Lab +-------------------- + +.. include:: include/src_clone_uwlab.rst + +.. include:: include/src_build_uwlab.rst + +.. include:: include/src_verify_uwlab.rst diff --git a/docs/source/setup/installation/source_installation.rst b/docs/source/setup/installation/source_installation.rst new file mode 100644 index 0000000..e5869aa --- /dev/null +++ b/docs/source/setup/installation/source_installation.rst @@ -0,0 +1,109 @@ +.. _uwlab-source-installation: + +Installation using Isaac Sim Source Code +======================================== + +The following steps first installs Isaac Sim from source, then UW Lab from source code. + +.. note:: + + This is a more advanced installation method and is not recommended for most users. Only follow this method + if you wish to modify the source code of Isaac Sim as well. + +Installing Isaac Sim +-------------------- + +Building from source +~~~~~~~~~~~~~~~~~~~~ + +From Isaac Sim 5.0 release, it is possible to build Isaac Sim from its source code. +This approach is meant for users who wish to modify the source code of Isaac Sim as well, +or want to test UW Lab with the nightly version of Isaac Sim. + +The following instructions are adapted from the `Isaac Sim documentation `_ +for the convenience of users. + +.. attention:: + + Building Isaac Sim from source requires Ubuntu 22.04 LTS or higher. + +.. attention:: + + For details on driver requirements, please see the `Technical Requirements `_ guide! + + On Windows, it may be necessary to `enable long path support `_ to avoid installation errors due to OS limitations. + + +- Clone the Isaac Sim repository into your workspace: + + .. code:: bash + + git clone https://github.com/isaac-sim/IsaacSim.git + +- Build Isaac Sim from source: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + cd IsaacSim + ./build.sh + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: bash + + cd IsaacSim + build.bat + + +Verifying the Isaac Sim installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To avoid the overhead of finding and locating the Isaac Sim installation +directory every time, we recommend exporting the following environment +variables to your terminal for the remaining of the installation instructions: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Isaac Sim root directory + export ISAACSIM_PATH="${pwd}/_build/linux-x86_64/release" + # Isaac Sim python executable + export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Isaac Sim root directory + set ISAACSIM_PATH="%cd%\_build\windows-x86_64\release" + :: Isaac Sim python executable + set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat" + +.. include:: include/bin_verify_isaacsim.rst + + +Installing UW Lab +-------------------- + +.. include:: include/src_clone_uwlab.rst + +.. include:: include/src_symlink_isaacsim.rst + +.. include:: include/src_python_virtual_env.rst + +.. include:: include/src_build_uwlab.rst + +.. include:: include/src_verify_uwlab.rst diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..1f51ba0 --- /dev/null +++ b/environment.yml @@ -0,0 +1,11 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +channels: + - conda-forge + - defaults +dependencies: + - python=3.11 + - importlib_metadata diff --git a/pyproject.toml b/pyproject.toml index aa7e6b3..d5fd925 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,15 +53,12 @@ known_third_party = [ "Semantics", ] # Imports from this repository -known_first_party = ["isaaclab", "uwlab"] -known_assets_firstparty = ["isaaclab_assets", "uwlab_assets"] +known_first_party = "uwlab" +known_assets_firstparty = "uwlab_assets" known_extra_firstparty = [ - "isaaclab_rl", - "isaaclab_mimic", "uwlab_rl", - "uwlab_apps" ] -known_task_firstparty = ["isaaclab_tasks", "uwlab_tasks"] +known_task_firstparty = "uwlab_tasks" # Imports from the local folder known_local_folder = "config" @@ -78,7 +75,7 @@ exclude = [ ] typeCheckingMode = "basic" -pythonVersion = "3.10" +pythonVersion = "3.11" pythonPlatform = "Linux" enableTypeIgnoreComments = true @@ -97,4 +94,4 @@ reportPrivateUsage = "warning" skip = '*.usd,*.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" +ignore-words-list = "haa,slq,collapsable,buss,reacher" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..dd4d14d --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +markers = + isaacsim_ci: mark test to run in isaacsim ci diff --git a/scripts/environments/export_IODescriptors.py b/scripts/environments/export_IODescriptors.py new file mode 100644 index 0000000..ba57336 --- /dev/null +++ b/scripts/environments/export_IODescriptors.py @@ -0,0 +1,102 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to an environment with random action agent.""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse +import os + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Random agent for Isaac Lab environments.") +parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument("--output_dir", type=str, default=None, help="Path to the output directory.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() +args_cli.headless = True + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import gymnasium as gym +import torch + +import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 +from isaaclab_tasks.utils import parse_env_cfg + +# PLACEHOLDER: Extension template (do not remove this comment) + + +def main(): + """Random actions agent with Isaac Lab environment.""" + # create environment configuration + env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1, use_fabric=True) + # create environment + env = gym.make(args_cli.task, cfg=env_cfg) + + # print info (this is vectorized environment) + print(f"[INFO]: Gym observation space: {env.observation_space}") + print(f"[INFO]: Gym action space: {env.action_space}") + # reset environment + env.reset() + + outs = env.unwrapped.get_IO_descriptors + out_observations = outs["observations"] + out_actions = outs["actions"] + out_articulations = outs["articulations"] + out_scene = outs["scene"] + # Make a yaml file with the output + import yaml + + name = args_cli.task.lower().replace("-", "_") + name = name.replace(" ", "_") + + if not os.path.exists(args_cli.output_dir): + os.makedirs(args_cli.output_dir) + + with open(os.path.join(args_cli.output_dir, f"{name}_IO_descriptors.yaml"), "w") as f: + print(f"[INFO]: Exporting IO descriptors to {os.path.join(args_cli.output_dir, f'{name}_IO_descriptors.yaml')}") + yaml.safe_dump(outs, f) + + for k in out_actions: + print(f"--- Action term: {k['name']} ---") + k.pop("name") + for k1, v1 in k.items(): + print(f"{k1}: {v1}") + + for obs_group_name, obs_group in out_observations.items(): + print(f"--- Obs group: {obs_group_name} ---") + for k in obs_group: + print(f"--- Obs term: {k['name']} ---") + k.pop("name") + for k1, v1 in k.items(): + print(f"{k1}: {v1}") + + for articulation_name, articulation_data in out_articulations.items(): + print(f"--- Articulation: {articulation_name} ---") + for k1, v1 in articulation_data.items(): + print(f"{k1}: {v1}") + + for k1, v1 in out_scene.items(): + print(f"{k1}: {v1}") + + env.step(torch.zeros(env.action_space.shape, device=env.unwrapped.device)) + env.close() + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/environments/list_envs.py b/scripts/environments/list_envs.py index 8452eb9..5c07438 100644 --- a/scripts/environments/list_envs.py +++ b/scripts/environments/list_envs.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -50,7 +45,7 @@ def main(): index = 0 # acquire all Isaac environments names for task_spec in gym.registry.values(): - if "Isaac" in task_spec.id or "UW" in task_spec.id or "UW" in task_spec.id: + if "Isaac" in task_spec.id: # add details to table table.add_row([index + 1, task_spec.id, task_spec.entry_point, task_spec.kwargs["env_cfg_entry_point"]]) # increment count diff --git a/scripts/environments/random_agent.py b/scripts/environments/random_agent.py index 46b4109..bec9817 100644 --- a/scripts/environments/random_agent.py +++ b/scripts/environments/random_agent.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -38,8 +33,11 @@ import torch import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 from isaaclab_tasks.utils import parse_env_cfg +# PLACEHOLDER: Extension template (do not remove this comment) + def main(): """Random actions agent with Isaac Lab environment.""" diff --git a/scripts/environments/state_machine/lift_cube_sm.py b/scripts/environments/state_machine/lift_cube_sm.py new file mode 100644 index 0000000..4a2b6bc --- /dev/null +++ b/scripts/environments/state_machine/lift_cube_sm.py @@ -0,0 +1,320 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Script to run an environment with a pick and lift state machine. + +The state machine is implemented in the kernel function `infer_state_machine`. +It uses the `warp` library to run the state machine in parallel on the GPU. + +.. code-block:: bash + + ./isaaclab.sh -p scripts/environments/state_machine/lift_cube_sm.py --num_envs 32 + +""" + +"""Launch Omniverse Toolkit first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Pick and lift state machine for lift environments.") +parser.add_argument( + "--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations." +) +parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(headless=args_cli.headless) +simulation_app = app_launcher.app + +"""Rest everything else.""" + +import gymnasium as gym +import torch +from collections.abc import Sequence + +import warp as wp + +from isaaclab.assets.rigid_object.rigid_object_data import RigidObjectData + +import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 +from isaaclab_tasks.manager_based.manipulation.lift.lift_env_cfg import LiftEnvCfg +from isaaclab_tasks.utils.parse_cfg import parse_env_cfg + +# initialize warp +wp.init() + + +class GripperState: + """States for the gripper.""" + + OPEN = wp.constant(1.0) + CLOSE = wp.constant(-1.0) + + +class PickSmState: + """States for the pick state machine.""" + + REST = wp.constant(0) + APPROACH_ABOVE_OBJECT = wp.constant(1) + APPROACH_OBJECT = wp.constant(2) + GRASP_OBJECT = wp.constant(3) + LIFT_OBJECT = wp.constant(4) + + +class PickSmWaitTime: + """Additional wait times (in s) for states for before switching.""" + + REST = wp.constant(0.2) + APPROACH_ABOVE_OBJECT = wp.constant(0.5) + APPROACH_OBJECT = wp.constant(0.6) + GRASP_OBJECT = wp.constant(0.3) + LIFT_OBJECT = wp.constant(1.0) + + +@wp.func +def distance_below_threshold(current_pos: wp.vec3, desired_pos: wp.vec3, threshold: float) -> bool: + return wp.length(current_pos - desired_pos) < threshold + + +@wp.kernel +def infer_state_machine( + dt: wp.array(dtype=float), + sm_state: wp.array(dtype=int), + sm_wait_time: wp.array(dtype=float), + ee_pose: wp.array(dtype=wp.transform), + object_pose: wp.array(dtype=wp.transform), + des_object_pose: wp.array(dtype=wp.transform), + des_ee_pose: wp.array(dtype=wp.transform), + gripper_state: wp.array(dtype=float), + offset: wp.array(dtype=wp.transform), + position_threshold: float, +): + # retrieve thread id + tid = wp.tid() + # retrieve state machine state + state = sm_state[tid] + # decide next state + if state == PickSmState.REST: + des_ee_pose[tid] = ee_pose[tid] + gripper_state[tid] = GripperState.OPEN + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.REST: + # move to next state and reset wait time + sm_state[tid] = PickSmState.APPROACH_ABOVE_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.APPROACH_ABOVE_OBJECT: + des_ee_pose[tid] = wp.transform_multiply(offset[tid], object_pose[tid]) + gripper_state[tid] = GripperState.OPEN + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.APPROACH_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.APPROACH_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.APPROACH_OBJECT: + des_ee_pose[tid] = object_pose[tid] + gripper_state[tid] = GripperState.OPEN + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + if sm_wait_time[tid] >= PickSmWaitTime.APPROACH_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.GRASP_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.GRASP_OBJECT: + des_ee_pose[tid] = object_pose[tid] + gripper_state[tid] = GripperState.CLOSE + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.GRASP_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.LIFT_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.LIFT_OBJECT: + des_ee_pose[tid] = des_object_pose[tid] + gripper_state[tid] = GripperState.CLOSE + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.LIFT_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.LIFT_OBJECT + sm_wait_time[tid] = 0.0 + # increment wait time + sm_wait_time[tid] = sm_wait_time[tid] + dt[tid] + + +class PickAndLiftSm: + """A simple state machine in a robot's task space to pick and lift an object. + + The state machine is implemented as a warp kernel. It takes in the current state of + the robot's end-effector and the object, and outputs the desired state of the robot's + end-effector and the gripper. The state machine is implemented as a finite state + machine with the following states: + + 1. REST: The robot is at rest. + 2. APPROACH_ABOVE_OBJECT: The robot moves above the object. + 3. APPROACH_OBJECT: The robot moves to the object. + 4. GRASP_OBJECT: The robot grasps the object. + 5. LIFT_OBJECT: The robot lifts the object to the desired pose. This is the final state. + """ + + def __init__(self, dt: float, num_envs: int, device: torch.device | str = "cpu", position_threshold=0.01): + """Initialize the state machine. + + Args: + dt: The environment time step. + num_envs: The number of environments to simulate. + device: The device to run the state machine on. + """ + # save parameters + self.dt = float(dt) + self.num_envs = num_envs + self.device = device + self.position_threshold = position_threshold + # initialize state machine + self.sm_dt = torch.full((self.num_envs,), self.dt, device=self.device) + self.sm_state = torch.full((self.num_envs,), 0, dtype=torch.int32, device=self.device) + self.sm_wait_time = torch.zeros((self.num_envs,), device=self.device) + + # desired state + self.des_ee_pose = torch.zeros((self.num_envs, 7), device=self.device) + self.des_gripper_state = torch.full((self.num_envs,), 0.0, device=self.device) + + # approach above object offset + self.offset = torch.zeros((self.num_envs, 7), device=self.device) + self.offset[:, 2] = 0.1 + self.offset[:, -1] = 1.0 # warp expects quaternion as (x, y, z, w) + + # convert to warp + self.sm_dt_wp = wp.from_torch(self.sm_dt, wp.float32) + self.sm_state_wp = wp.from_torch(self.sm_state, wp.int32) + self.sm_wait_time_wp = wp.from_torch(self.sm_wait_time, wp.float32) + self.des_ee_pose_wp = wp.from_torch(self.des_ee_pose, wp.transform) + self.des_gripper_state_wp = wp.from_torch(self.des_gripper_state, wp.float32) + self.offset_wp = wp.from_torch(self.offset, wp.transform) + + def reset_idx(self, env_ids: Sequence[int] = None): + """Reset the state machine.""" + if env_ids is None: + env_ids = slice(None) + self.sm_state[env_ids] = 0 + self.sm_wait_time[env_ids] = 0.0 + + def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_pose: torch.Tensor) -> torch.Tensor: + """Compute the desired state of the robot's end-effector and the gripper.""" + # convert all transformations from (w, x, y, z) to (x, y, z, w) + ee_pose = ee_pose[:, [0, 1, 2, 4, 5, 6, 3]] + object_pose = object_pose[:, [0, 1, 2, 4, 5, 6, 3]] + des_object_pose = des_object_pose[:, [0, 1, 2, 4, 5, 6, 3]] + + # convert to warp + ee_pose_wp = wp.from_torch(ee_pose.contiguous(), wp.transform) + object_pose_wp = wp.from_torch(object_pose.contiguous(), wp.transform) + des_object_pose_wp = wp.from_torch(des_object_pose.contiguous(), wp.transform) + + # run state machine + wp.launch( + kernel=infer_state_machine, + dim=self.num_envs, + inputs=[ + self.sm_dt_wp, + self.sm_state_wp, + self.sm_wait_time_wp, + ee_pose_wp, + object_pose_wp, + des_object_pose_wp, + self.des_ee_pose_wp, + self.des_gripper_state_wp, + self.offset_wp, + self.position_threshold, + ], + device=self.device, + ) + + # convert transformations back to (w, x, y, z) + des_ee_pose = self.des_ee_pose[:, [0, 1, 2, 6, 3, 4, 5]] + # convert to torch + return torch.cat([des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) + + +def main(): + # parse configuration + env_cfg: LiftEnvCfg = parse_env_cfg( + "Isaac-Lift-Cube-Franka-IK-Abs-v0", + device=args_cli.device, + num_envs=args_cli.num_envs, + use_fabric=not args_cli.disable_fabric, + ) + # create environment + env = gym.make("Isaac-Lift-Cube-Franka-IK-Abs-v0", cfg=env_cfg) + # reset environment at start + env.reset() + + # create action buffers (position + quaternion) + actions = torch.zeros(env.unwrapped.action_space.shape, device=env.unwrapped.device) + actions[:, 3] = 1.0 + # desired object orientation (we only do position control of object) + desired_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) + desired_orientation[:, 1] = 1.0 + # create state machine + pick_sm = PickAndLiftSm( + env_cfg.sim.dt * env_cfg.decimation, env.unwrapped.num_envs, env.unwrapped.device, position_threshold=0.01 + ) + + while simulation_app.is_running(): + # run everything in inference mode + with torch.inference_mode(): + # step environment + dones = env.step(actions)[-2] + + # observations + # -- end-effector frame + ee_frame_sensor = env.unwrapped.scene["ee_frame"] + tcp_rest_position = ee_frame_sensor.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins + tcp_rest_orientation = ee_frame_sensor.data.target_quat_w[..., 0, :].clone() + # -- object frame + object_data: RigidObjectData = env.unwrapped.scene["object"].data + object_position = object_data.root_pos_w - env.unwrapped.scene.env_origins + # -- target object frame + desired_position = env.unwrapped.command_manager.get_command("object_pose")[..., :3] + + # advance state machine + actions = pick_sm.compute( + torch.cat([tcp_rest_position, tcp_rest_orientation], dim=-1), + torch.cat([object_position, desired_orientation], dim=-1), + torch.cat([desired_position, desired_orientation], dim=-1), + ) + + # reset state machine + if dones.any(): + pick_sm.reset_idx(dones.nonzero(as_tuple=False).squeeze(-1)) + + # close the environment + env.close() + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/environments/state_machine/lift_teddy_bear.py b/scripts/environments/state_machine/lift_teddy_bear.py new file mode 100644 index 0000000..e1a572f --- /dev/null +++ b/scripts/environments/state_machine/lift_teddy_bear.py @@ -0,0 +1,342 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Script to demonstrate lifting a deformable object with a robotic arm. + +The state machine is implemented in the kernel function `infer_state_machine`. +It uses the `warp` library to run the state machine in parallel on the GPU. + +.. code-block:: bash + + ./isaaclab.sh -p scripts/environments/state_machine/lift_teddy_bear.py + +""" + +"""Launch Omniverse Toolkit first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Pick and lift a teddy bear with a robotic arm.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(headless=args_cli.headless) +simulation_app = app_launcher.app + +# disable metrics assembler due to scene graph instancing +from isaacsim.core.utils.extensions import disable_extension + +disable_extension("omni.usd.metrics.assembler.ui") + +"""Rest everything else.""" + +import gymnasium as gym +import torch +from collections.abc import Sequence + +import warp as wp + +from isaaclab.assets.rigid_object.rigid_object_data import RigidObjectData + +import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 +from isaaclab_tasks.manager_based.manipulation.lift.lift_env_cfg import LiftEnvCfg +from isaaclab_tasks.utils.parse_cfg import parse_env_cfg + +# initialize warp +wp.init() + + +class GripperState: + """States for the gripper.""" + + OPEN = wp.constant(1.0) + CLOSE = wp.constant(-1.0) + + +class PickSmState: + """States for the pick state machine.""" + + REST = wp.constant(0) + APPROACH_ABOVE_OBJECT = wp.constant(1) + APPROACH_OBJECT = wp.constant(2) + GRASP_OBJECT = wp.constant(3) + LIFT_OBJECT = wp.constant(4) + OPEN_GRIPPER = wp.constant(5) + + +class PickSmWaitTime: + """Additional wait times (in s) for states for before switching.""" + + REST = wp.constant(0.2) + APPROACH_ABOVE_OBJECT = wp.constant(0.5) + APPROACH_OBJECT = wp.constant(0.6) + GRASP_OBJECT = wp.constant(0.6) + LIFT_OBJECT = wp.constant(1.0) + OPEN_GRIPPER = wp.constant(0.0) + + +@wp.func +def distance_below_threshold(current_pos: wp.vec3, desired_pos: wp.vec3, threshold: float) -> bool: + return wp.length(current_pos - desired_pos) < threshold + + +@wp.kernel +def infer_state_machine( + dt: wp.array(dtype=float), + sm_state: wp.array(dtype=int), + sm_wait_time: wp.array(dtype=float), + ee_pose: wp.array(dtype=wp.transform), + object_pose: wp.array(dtype=wp.transform), + des_object_pose: wp.array(dtype=wp.transform), + des_ee_pose: wp.array(dtype=wp.transform), + gripper_state: wp.array(dtype=float), + offset: wp.array(dtype=wp.transform), + position_threshold: float, +): + # retrieve thread id + tid = wp.tid() + # retrieve state machine state + state = sm_state[tid] + # decide next state + if state == PickSmState.REST: + des_ee_pose[tid] = ee_pose[tid] + gripper_state[tid] = GripperState.OPEN + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.REST: + # move to next state and reset wait time + sm_state[tid] = PickSmState.APPROACH_ABOVE_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.APPROACH_ABOVE_OBJECT: + des_ee_pose[tid] = wp.transform_multiply(offset[tid], object_pose[tid]) + gripper_state[tid] = GripperState.OPEN + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.APPROACH_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.APPROACH_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.APPROACH_OBJECT: + des_ee_pose[tid] = object_pose[tid] + gripper_state[tid] = GripperState.OPEN + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.APPROACH_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.GRASP_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.GRASP_OBJECT: + des_ee_pose[tid] = object_pose[tid] + gripper_state[tid] = GripperState.CLOSE + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.GRASP_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.LIFT_OBJECT + sm_wait_time[tid] = 0.0 + elif state == PickSmState.LIFT_OBJECT: + des_ee_pose[tid] = des_object_pose[tid] + gripper_state[tid] = GripperState.CLOSE + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.LIFT_OBJECT: + # move to next state and reset wait time + sm_state[tid] = PickSmState.OPEN_GRIPPER + sm_wait_time[tid] = 0.0 + elif state == PickSmState.OPEN_GRIPPER: + # des_ee_pose[tid] = object_pose[tid] + gripper_state[tid] = GripperState.OPEN + # wait for a while + if sm_wait_time[tid] >= PickSmWaitTime.OPEN_GRIPPER: + # move to next state and reset wait time + sm_state[tid] = PickSmState.OPEN_GRIPPER + sm_wait_time[tid] = 0.0 + # increment wait time + sm_wait_time[tid] = sm_wait_time[tid] + dt[tid] + + +class PickAndLiftSm: + """A simple state machine in a robot's task space to pick and lift an object. + + The state machine is implemented as a warp kernel. It takes in the current state of + the robot's end-effector and the object, and outputs the desired state of the robot's + end-effector and the gripper. The state machine is implemented as a finite state + machine with the following states: + + 1. REST: The robot is at rest. + 2. APPROACH_ABOVE_OBJECT: The robot moves above the object. + 3. APPROACH_OBJECT: The robot moves to the object. + 4. GRASP_OBJECT: The robot grasps the object. + 5. LIFT_OBJECT: The robot lifts the object to the desired pose. This is the final state. + """ + + def __init__(self, dt: float, num_envs: int, device: torch.device | str = "cpu", position_threshold=0.01): + """Initialize the state machine. + + Args: + dt: The environment time step. + num_envs: The number of environments to simulate. + device: The device to run the state machine on. + """ + # save parameters + self.dt = float(dt) + self.num_envs = num_envs + self.device = device + self.position_threshold = position_threshold + # initialize state machine + self.sm_dt = torch.full((self.num_envs,), self.dt, device=self.device) + self.sm_state = torch.full((self.num_envs,), 0, dtype=torch.int32, device=self.device) + self.sm_wait_time = torch.zeros((self.num_envs,), device=self.device) + + # desired state + self.des_ee_pose = torch.zeros((self.num_envs, 7), device=self.device) + self.des_gripper_state = torch.full((self.num_envs,), 0.0, device=self.device) + + # approach above object offset + self.offset = torch.zeros((self.num_envs, 7), device=self.device) + self.offset[:, 2] = 0.2 + self.offset[:, -1] = 1.0 # warp expects quaternion as (x, y, z, w) + + # convert to warp + self.sm_dt_wp = wp.from_torch(self.sm_dt, wp.float32) + self.sm_state_wp = wp.from_torch(self.sm_state, wp.int32) + self.sm_wait_time_wp = wp.from_torch(self.sm_wait_time, wp.float32) + self.des_ee_pose_wp = wp.from_torch(self.des_ee_pose, wp.transform) + self.des_gripper_state_wp = wp.from_torch(self.des_gripper_state, wp.float32) + self.offset_wp = wp.from_torch(self.offset, wp.transform) + + def reset_idx(self, env_ids: Sequence[int] = None): + """Reset the state machine.""" + if env_ids is None: + env_ids = slice(None) + self.sm_state[env_ids] = 0 + self.sm_wait_time[env_ids] = 0.0 + + def compute(self, ee_pose: torch.Tensor, object_pose: torch.Tensor, des_object_pose: torch.Tensor): + """Compute the desired state of the robot's end-effector and the gripper.""" + # convert all transformations from (w, x, y, z) to (x, y, z, w) + ee_pose = ee_pose[:, [0, 1, 2, 4, 5, 6, 3]] + object_pose = object_pose[:, [0, 1, 2, 4, 5, 6, 3]] + des_object_pose = des_object_pose[:, [0, 1, 2, 4, 5, 6, 3]] + + # convert to warp + ee_pose_wp = wp.from_torch(ee_pose.contiguous(), wp.transform) + object_pose_wp = wp.from_torch(object_pose.contiguous(), wp.transform) + des_object_pose_wp = wp.from_torch(des_object_pose.contiguous(), wp.transform) + + # run state machine + wp.launch( + kernel=infer_state_machine, + dim=self.num_envs, + inputs=[ + self.sm_dt_wp, + self.sm_state_wp, + self.sm_wait_time_wp, + ee_pose_wp, + object_pose_wp, + des_object_pose_wp, + self.des_ee_pose_wp, + self.des_gripper_state_wp, + self.offset_wp, + self.position_threshold, + ], + device=self.device, + ) + + # convert transformations back to (w, x, y, z) + des_ee_pose = self.des_ee_pose[:, [0, 1, 2, 6, 3, 4, 5]] + # convert to torch + return torch.cat([des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) + + +def main(): + # parse configuration + env_cfg: LiftEnvCfg = parse_env_cfg( + "Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0", + device=args_cli.device, + num_envs=args_cli.num_envs, + ) + + env_cfg.viewer.eye = (2.1, 1.0, 1.3) + + # create environment + env = gym.make("Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0", cfg=env_cfg) + # reset environment at start + env.reset() + + # create action buffers (position + quaternion) + actions = torch.zeros(env.unwrapped.action_space.shape, device=env.unwrapped.device) + actions[:, 3] = 1.0 + # desired rotation after grasping + desired_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) + desired_orientation[:, 1] = 1.0 + + object_grasp_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) + # z-axis pointing down and 45 degrees rotation + object_grasp_orientation[:, 1] = 0.9238795 + object_grasp_orientation[:, 2] = -0.3826834 + object_local_grasp_position = torch.tensor([0.02, -0.08, 0.0], device=env.unwrapped.device) + + # create state machine + pick_sm = PickAndLiftSm(env_cfg.sim.dt * env_cfg.decimation, env.unwrapped.num_envs, env.unwrapped.device) + + while simulation_app.is_running(): + # run everything in inference mode + with torch.inference_mode(): + # step environment + dones = env.step(actions)[-2] + + # observations + # -- end-effector frame + ee_frame_sensor = env.unwrapped.scene["ee_frame"] + tcp_rest_position = ee_frame_sensor.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins + tcp_rest_orientation = ee_frame_sensor.data.target_quat_w[..., 0, :].clone() + # -- object frame + object_data: RigidObjectData = env.unwrapped.scene["object"].data + object_position = object_data.root_pos_w - env.unwrapped.scene.env_origins + object_position += object_local_grasp_position + + # -- target object frame + desired_position = env.unwrapped.command_manager.get_command("object_pose")[..., :3] + + # advance state machine + actions = pick_sm.compute( + torch.cat([tcp_rest_position, tcp_rest_orientation], dim=-1), + torch.cat([object_position, object_grasp_orientation], dim=-1), + torch.cat([desired_position, desired_orientation], dim=-1), + ) + + # reset state machine + if dones.any(): + pick_sm.reset_idx(dones.nonzero(as_tuple=False).squeeze(-1)) + + # close the environment + env.close() + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/environments/state_machine/open_cabinet_sm.py b/scripts/environments/state_machine/open_cabinet_sm.py new file mode 100644 index 0000000..9ddeb0f --- /dev/null +++ b/scripts/environments/state_machine/open_cabinet_sm.py @@ -0,0 +1,334 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Script to run an environment with a cabinet opening state machine. + +The state machine is implemented in the kernel function `infer_state_machine`. +It uses the `warp` library to run the state machine in parallel on the GPU. + +.. code-block:: bash + + ./isaaclab.sh -p scripts/environments/state_machine/open_cabinet_sm.py --num_envs 32 + +""" + +"""Launch Omniverse Toolkit first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Pick and lift state machine for cabinet environments.") +parser.add_argument( + "--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations." +) +parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(headless=args_cli.headless) +simulation_app = app_launcher.app + +"""Rest everything else.""" + +import gymnasium as gym +import torch +from collections.abc import Sequence + +import warp as wp + +from isaaclab.sensors import FrameTransformer + +import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 +from isaaclab_tasks.manager_based.manipulation.cabinet.cabinet_env_cfg import CabinetEnvCfg +from isaaclab_tasks.utils.parse_cfg import parse_env_cfg + +# initialize warp +wp.init() + + +class GripperState: + """States for the gripper.""" + + OPEN = wp.constant(1.0) + CLOSE = wp.constant(-1.0) + + +class OpenDrawerSmState: + """States for the cabinet drawer opening state machine.""" + + REST = wp.constant(0) + APPROACH_INFRONT_HANDLE = wp.constant(1) + APPROACH_HANDLE = wp.constant(2) + GRASP_HANDLE = wp.constant(3) + OPEN_DRAWER = wp.constant(4) + RELEASE_HANDLE = wp.constant(5) + + +class OpenDrawerSmWaitTime: + """Additional wait times (in s) for states for before switching.""" + + REST = wp.constant(0.5) + APPROACH_INFRONT_HANDLE = wp.constant(1.25) + APPROACH_HANDLE = wp.constant(1.0) + GRASP_HANDLE = wp.constant(1.0) + OPEN_DRAWER = wp.constant(3.0) + RELEASE_HANDLE = wp.constant(0.2) + + +@wp.func +def distance_below_threshold(current_pos: wp.vec3, desired_pos: wp.vec3, threshold: float) -> bool: + return wp.length(current_pos - desired_pos) < threshold + + +@wp.kernel +def infer_state_machine( + dt: wp.array(dtype=float), + sm_state: wp.array(dtype=int), + sm_wait_time: wp.array(dtype=float), + ee_pose: wp.array(dtype=wp.transform), + handle_pose: wp.array(dtype=wp.transform), + des_ee_pose: wp.array(dtype=wp.transform), + gripper_state: wp.array(dtype=float), + handle_approach_offset: wp.array(dtype=wp.transform), + handle_grasp_offset: wp.array(dtype=wp.transform), + drawer_opening_rate: wp.array(dtype=wp.transform), + position_threshold: float, +): + # retrieve thread id + tid = wp.tid() + # retrieve state machine state + state = sm_state[tid] + # decide next state + if state == OpenDrawerSmState.REST: + des_ee_pose[tid] = ee_pose[tid] + gripper_state[tid] = GripperState.OPEN + # wait for a while + if sm_wait_time[tid] >= OpenDrawerSmWaitTime.REST: + # move to next state and reset wait time + sm_state[tid] = OpenDrawerSmState.APPROACH_INFRONT_HANDLE + sm_wait_time[tid] = 0.0 + elif state == OpenDrawerSmState.APPROACH_INFRONT_HANDLE: + des_ee_pose[tid] = wp.transform_multiply(handle_approach_offset[tid], handle_pose[tid]) + gripper_state[tid] = GripperState.OPEN + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + # wait for a while + if sm_wait_time[tid] >= OpenDrawerSmWaitTime.APPROACH_INFRONT_HANDLE: + # move to next state and reset wait time + sm_state[tid] = OpenDrawerSmState.APPROACH_HANDLE + sm_wait_time[tid] = 0.0 + elif state == OpenDrawerSmState.APPROACH_HANDLE: + des_ee_pose[tid] = handle_pose[tid] + gripper_state[tid] = GripperState.OPEN + if distance_below_threshold( + wp.transform_get_translation(ee_pose[tid]), + wp.transform_get_translation(des_ee_pose[tid]), + position_threshold, + ): + # wait for a while + if sm_wait_time[tid] >= OpenDrawerSmWaitTime.APPROACH_HANDLE: + # move to next state and reset wait time + sm_state[tid] = OpenDrawerSmState.GRASP_HANDLE + sm_wait_time[tid] = 0.0 + elif state == OpenDrawerSmState.GRASP_HANDLE: + des_ee_pose[tid] = wp.transform_multiply(handle_grasp_offset[tid], handle_pose[tid]) + gripper_state[tid] = GripperState.CLOSE + # wait for a while + if sm_wait_time[tid] >= OpenDrawerSmWaitTime.GRASP_HANDLE: + # move to next state and reset wait time + sm_state[tid] = OpenDrawerSmState.OPEN_DRAWER + sm_wait_time[tid] = 0.0 + elif state == OpenDrawerSmState.OPEN_DRAWER: + des_ee_pose[tid] = wp.transform_multiply(drawer_opening_rate[tid], handle_pose[tid]) + gripper_state[tid] = GripperState.CLOSE + # wait for a while + if sm_wait_time[tid] >= OpenDrawerSmWaitTime.OPEN_DRAWER: + # move to next state and reset wait time + sm_state[tid] = OpenDrawerSmState.RELEASE_HANDLE + sm_wait_time[tid] = 0.0 + elif state == OpenDrawerSmState.RELEASE_HANDLE: + des_ee_pose[tid] = ee_pose[tid] + gripper_state[tid] = GripperState.CLOSE + # wait for a while + if sm_wait_time[tid] >= OpenDrawerSmWaitTime.RELEASE_HANDLE: + # move to next state and reset wait time + sm_state[tid] = OpenDrawerSmState.RELEASE_HANDLE + sm_wait_time[tid] = 0.0 + # increment wait time + sm_wait_time[tid] = sm_wait_time[tid] + dt[tid] + + +class OpenDrawerSm: + """A simple state machine in a robot's task space to open a drawer in the cabinet. + + The state machine is implemented as a warp kernel. It takes in the current state of + the robot's end-effector and the object, and outputs the desired state of the robot's + end-effector and the gripper. The state machine is implemented as a finite state + machine with the following states: + + 1. REST: The robot is at rest. + 2. APPROACH_HANDLE: The robot moves towards the handle of the drawer. + 3. GRASP_HANDLE: The robot grasps the handle of the drawer. + 4. OPEN_DRAWER: The robot opens the drawer. + 5. RELEASE_HANDLE: The robot releases the handle of the drawer. This is the final state. + """ + + def __init__(self, dt: float, num_envs: int, device: torch.device | str = "cpu", position_threshold=0.01): + """Initialize the state machine. + + Args: + dt: The environment time step. + num_envs: The number of environments to simulate. + device: The device to run the state machine on. + """ + # save parameters + self.dt = float(dt) + self.num_envs = num_envs + self.device = device + self.position_threshold = position_threshold + # initialize state machine + self.sm_dt = torch.full((self.num_envs,), self.dt, device=self.device) + self.sm_state = torch.full((self.num_envs,), 0, dtype=torch.int32, device=self.device) + self.sm_wait_time = torch.zeros((self.num_envs,), device=self.device) + + # desired state + self.des_ee_pose = torch.zeros((self.num_envs, 7), device=self.device) + self.des_gripper_state = torch.full((self.num_envs,), 0.0, device=self.device) + + # approach in front of the handle + self.handle_approach_offset = torch.zeros((self.num_envs, 7), device=self.device) + self.handle_approach_offset[:, 0] = -0.1 + self.handle_approach_offset[:, -1] = 1.0 # warp expects quaternion as (x, y, z, w) + + # handle grasp offset + self.handle_grasp_offset = torch.zeros((self.num_envs, 7), device=self.device) + self.handle_grasp_offset[:, 0] = 0.025 + self.handle_grasp_offset[:, -1] = 1.0 # warp expects quaternion as (x, y, z, w) + + # drawer opening rate + self.drawer_opening_rate = torch.zeros((self.num_envs, 7), device=self.device) + self.drawer_opening_rate[:, 0] = -0.015 + self.drawer_opening_rate[:, -1] = 1.0 # warp expects quaternion as (x, y, z, w) + + # convert to warp + self.sm_dt_wp = wp.from_torch(self.sm_dt, wp.float32) + self.sm_state_wp = wp.from_torch(self.sm_state, wp.int32) + self.sm_wait_time_wp = wp.from_torch(self.sm_wait_time, wp.float32) + self.des_ee_pose_wp = wp.from_torch(self.des_ee_pose, wp.transform) + self.des_gripper_state_wp = wp.from_torch(self.des_gripper_state, wp.float32) + self.handle_approach_offset_wp = wp.from_torch(self.handle_approach_offset, wp.transform) + self.handle_grasp_offset_wp = wp.from_torch(self.handle_grasp_offset, wp.transform) + self.drawer_opening_rate_wp = wp.from_torch(self.drawer_opening_rate, wp.transform) + + def reset_idx(self, env_ids: Sequence[int] | None = None): + """Reset the state machine.""" + if env_ids is None: + env_ids = slice(None) + # reset state machine + self.sm_state[env_ids] = 0 + self.sm_wait_time[env_ids] = 0.0 + + def compute(self, ee_pose: torch.Tensor, handle_pose: torch.Tensor): + """Compute the desired state of the robot's end-effector and the gripper.""" + # convert all transformations from (w, x, y, z) to (x, y, z, w) + ee_pose = ee_pose[:, [0, 1, 2, 4, 5, 6, 3]] + handle_pose = handle_pose[:, [0, 1, 2, 4, 5, 6, 3]] + # convert to warp + ee_pose_wp = wp.from_torch(ee_pose.contiguous(), wp.transform) + handle_pose_wp = wp.from_torch(handle_pose.contiguous(), wp.transform) + + # run state machine + wp.launch( + kernel=infer_state_machine, + dim=self.num_envs, + inputs=[ + self.sm_dt_wp, + self.sm_state_wp, + self.sm_wait_time_wp, + ee_pose_wp, + handle_pose_wp, + self.des_ee_pose_wp, + self.des_gripper_state_wp, + self.handle_approach_offset_wp, + self.handle_grasp_offset_wp, + self.drawer_opening_rate_wp, + self.position_threshold, + ], + device=self.device, + ) + + # convert transformations back to (w, x, y, z) + des_ee_pose = self.des_ee_pose[:, [0, 1, 2, 6, 3, 4, 5]] + # convert to torch + return torch.cat([des_ee_pose, self.des_gripper_state.unsqueeze(-1)], dim=-1) + + +def main(): + # parse configuration + env_cfg: CabinetEnvCfg = parse_env_cfg( + "Isaac-Open-Drawer-Franka-IK-Abs-v0", + device=args_cli.device, + num_envs=args_cli.num_envs, + use_fabric=not args_cli.disable_fabric, + ) + # create environment + env = gym.make("Isaac-Open-Drawer-Franka-IK-Abs-v0", cfg=env_cfg) + # reset environment at start + env.reset() + + # create action buffers (position + quaternion) + actions = torch.zeros(env.unwrapped.action_space.shape, device=env.unwrapped.device) + actions[:, 3] = 1.0 + # desired object orientation (we only do position control of object) + desired_orientation = torch.zeros((env.unwrapped.num_envs, 4), device=env.unwrapped.device) + desired_orientation[:, 1] = 1.0 + # create state machine + open_sm = OpenDrawerSm(env_cfg.sim.dt * env_cfg.decimation, env.unwrapped.num_envs, env.unwrapped.device) + + while simulation_app.is_running(): + # run everything in inference mode + with torch.inference_mode(): + # step environment + dones = env.step(actions)[-2] + + # observations + # -- end-effector frame + ee_frame_tf: FrameTransformer = env.unwrapped.scene["ee_frame"] + tcp_rest_position = ee_frame_tf.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins + tcp_rest_orientation = ee_frame_tf.data.target_quat_w[..., 0, :].clone() + # -- handle frame + cabinet_frame_tf: FrameTransformer = env.unwrapped.scene["cabinet_frame"] + cabinet_position = cabinet_frame_tf.data.target_pos_w[..., 0, :].clone() - env.unwrapped.scene.env_origins + cabinet_orientation = cabinet_frame_tf.data.target_quat_w[..., 0, :].clone() + + # advance state machine + actions = open_sm.compute( + torch.cat([tcp_rest_position, tcp_rest_orientation], dim=-1), + torch.cat([cabinet_position, cabinet_orientation], dim=-1), + ) + + # reset state machine + if dones.any(): + open_sm.reset_idx(dones.nonzero(as_tuple=False).squeeze(-1)) + + # close the environment + env.close() + + +if __name__ == "__main__": + # run the main execution + main() + # close sim app + simulation_app.close() diff --git a/scripts/environments/teleoperation/teleop_se3_agent.py b/scripts/environments/teleoperation/teleop_se3_agent.py new file mode 100644 index 0000000..43dfeb2 --- /dev/null +++ b/scripts/environments/teleoperation/teleop_se3_agent.py @@ -0,0 +1,273 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to run a keyboard teleoperation with Isaac Lab manipulation environments.""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse +from collections.abc import Callable + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Keyboard teleoperation for Isaac Lab environments.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") +parser.add_argument( + "--teleop_device", + type=str, + default="keyboard", + help=( + "Teleop device. Set here (legacy) or via the environment config. If using the environment config, pass the" + " device key/name defined under 'teleop_devices' (it can be a custom name, not necessarily 'handtracking')." + " Built-ins: keyboard, spacemouse, gamepad. Not all tasks support all built-ins." + ), +) +parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument("--sensitivity", type=float, default=1.0, help="Sensitivity factor.") +parser.add_argument( + "--enable_pinocchio", + action="store_true", + default=False, + help="Enable Pinocchio.", +) +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +app_launcher_args = vars(args_cli) + +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and + # not the one installed by Isaac Sim pinocchio is required by the Pink IK controllers and the + # GR1T2 retargeter + import pinocchio # noqa: F401 +if "handtracking" in args_cli.teleop_device.lower(): + app_launcher_args["xr"] = True + +# launch omniverse app +app_launcher = AppLauncher(app_launcher_args) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + + +import gymnasium as gym +import logging +import torch + +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.managers import TerminationTermCfg as DoneTerm + +import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 +from isaaclab_tasks.manager_based.manipulation.lift import mdp +from isaaclab_tasks.utils import parse_env_cfg + +if args_cli.enable_pinocchio: + import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 + import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 + +# import logger +logger = logging.getLogger(__name__) + + +def main() -> None: + """ + Run keyboard teleoperation with Isaac Lab manipulation environment. + + Creates the environment, sets up teleoperation interfaces and callbacks, + and runs the main simulation loop until the application is closed. + + Returns: + 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 + # modify configuration + env_cfg.terminations.time_out = None + if "Lift" in args_cli.task: + # set the resampling time range to large number to avoid resampling + env_cfg.commands.object_pose.resampling_time_range = (1.0e9, 1.0e9) + # add termination condition for reaching the goal otherwise the environment won't reset + env_cfg.terminations.object_reached_goal = DoneTerm(func=mdp.object_reached_goal) + + 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" + + try: + # create environment + 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: + logger.warning( + f"The environment '{args_cli.task}' does not support gripper control. The device command will be" + " ignored." + ) + except Exception as e: + logger.error(f"Failed to create environment: {e}") + simulation_app.close() + return + + # Flags for controlling teleoperation flow + should_reset_recording_instance = False + teleoperation_active = True + + # Callback handlers + def reset_recording_instance() -> None: + """ + Reset the environment to its initial state. + + Sets a flag to reset the environment on the next simulation step. + + Returns: + None + """ + nonlocal should_reset_recording_instance + should_reset_recording_instance = True + print("Reset triggered - Environment will reset on next step") + + def start_teleoperation() -> None: + """ + Activate teleoperation control of the robot. + + Enables the application of teleoperation commands to the environment. + + Returns: + None + """ + nonlocal teleoperation_active + teleoperation_active = True + print("Teleoperation activated") + + def stop_teleoperation() -> None: + """ + Deactivate teleoperation control of the robot. + + Disables the application of teleoperation commands to the environment. + + Returns: + None + """ + nonlocal teleoperation_active + teleoperation_active = False + print("Teleoperation deactivated") + + # Create device config if not already in env_cfg + teleoperation_callbacks: dict[str, Callable[[], None]] = { + "R": reset_recording_instance, + "START": start_teleoperation, + "STOP": stop_teleoperation, + "RESET": reset_recording_instance, + } + + # For hand tracking devices, add additional callbacks + if args_cli.xr: + # Default to inactive for hand tracking + teleoperation_active = False + else: + # Always active for other devices + teleoperation_active = True + + # Create teleop device from config if present, otherwise create manually + teleop_interface = None + try: + if hasattr(env_cfg, "teleop_devices") and args_cli.teleop_device in env_cfg.teleop_devices.devices: + teleop_interface = create_teleop_device( + args_cli.teleop_device, env_cfg.teleop_devices.devices, teleoperation_callbacks + ) + else: + logger.warning( + f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default." + ) + # Create fallback teleop device + sensitivity = args_cli.sensitivity + if args_cli.teleop_device.lower() == "keyboard": + teleop_interface = Se3Keyboard( + Se3KeyboardCfg(pos_sensitivity=0.05 * sensitivity, rot_sensitivity=0.05 * sensitivity) + ) + elif args_cli.teleop_device.lower() == "spacemouse": + teleop_interface = Se3SpaceMouse( + Se3SpaceMouseCfg(pos_sensitivity=0.05 * sensitivity, rot_sensitivity=0.05 * sensitivity) + ) + elif args_cli.teleop_device.lower() == "gamepad": + teleop_interface = Se3Gamepad( + Se3GamepadCfg(pos_sensitivity=0.1 * sensitivity, rot_sensitivity=0.1 * sensitivity) + ) + else: + logger.error(f"Unsupported teleop device: {args_cli.teleop_device}") + logger.error("Supported devices: keyboard, spacemouse, gamepad, handtracking") + env.close() + simulation_app.close() + return + + # Add callbacks to fallback device + for key, callback in teleoperation_callbacks.items(): + try: + teleop_interface.add_callback(key, callback) + except (ValueError, TypeError) as e: + logger.warning(f"Failed to add callback for key {key}: {e}") + except Exception as e: + logger.error(f"Failed to create teleop device: {e}") + env.close() + simulation_app.close() + return + + if teleop_interface is None: + logger.error("Failed to create teleop interface") + env.close() + simulation_app.close() + return + + print(f"Using teleop device: {teleop_interface}") + + # reset environment + env.reset() + teleop_interface.reset() + + print("Teleoperation started. Press 'R' to reset the environment.") + + # simulate environment + while simulation_app.is_running(): + try: + # run everything in inference mode + with torch.inference_mode(): + # get device command + action = teleop_interface.advance() + + # Only apply teleop commands when active + if teleoperation_active: + # process actions + actions = action.repeat(env.num_envs, 1) + # apply actions + env.step(actions) + else: + env.sim.render() + + if should_reset_recording_instance: + env.reset() + should_reset_recording_instance = False + print("Environment reset complete") + except Exception as e: + logger.error(f"Error during simulation step: {e}") + break + + # close the simulator + env.close() + print("Environment closed") + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/environments/zero_agent.py b/scripts/environments/zero_agent.py index 3d96e9a..88aa98e 100644 --- a/scripts/environments/zero_agent.py +++ b/scripts/environments/zero_agent.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -38,8 +33,11 @@ import torch import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 from isaaclab_tasks.utils import parse_env_cfg +# PLACEHOLDER: Extension template (do not remove this comment) + def main(): """Zero actions agent with Isaac Lab environment.""" diff --git a/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py b/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py index 88e2845..f67f0be 100644 --- a/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py +++ b/scripts/imitation_learning/isaaclab_mimic/annotate_demos.py @@ -1,23 +1,20 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 """ Script to add mimic annotations to demos to be used as source demos for mimic dataset generation. """ -# Launching Isaac Sim Simulator first. - import argparse +import math from isaaclab.app import AppLauncher +# Launching Isaac Sim Simulator first. + + # add argparse arguments parser = argparse.ArgumentParser(description="Annotate demonstrations for Isaac Lab environments.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") @@ -32,11 +29,16 @@ ) parser.add_argument("--auto", action="store_true", default=False, help="Automatically annotate subtasks.") parser.add_argument( - "--signals", - type=str, - nargs="+", - default=[], - help="Sequence of subtask termination signals for all except last subtask", + "--enable_pinocchio", + action="store_true", + default=False, + help="Enable Pinocchio.", +) +parser.add_argument( + "--annotate_subtask_start_signals", + action="store_true", + default=False, + help="Enable annotating start points of subtasks.", ) # append AppLauncher cli args @@ -44,6 +46,11 @@ # parse the arguments args_cli = parser.parse_args() +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim + # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter + import pinocchio # noqa: F401 + # launch the simulator app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -57,21 +64,27 @@ import isaaclab_mimic.envs # noqa: F401 +if args_cli.enable_pinocchio: + import isaaclab_mimic.envs.pinocchio_envs # noqa: F401 + # Only enables inputs if this script is NOT headless mode if not args_cli.headless and not os.environ.get("HEADLESS", 0): - from isaaclab.devices import Se3Keyboard + from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg + from isaaclab.envs import ManagerBasedRLMimicEnv from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg -from isaaclab.managers import RecorderTerm, RecorderTermCfg +from isaaclab.managers import RecorderTerm, RecorderTermCfg, TerminationTermCfg from isaaclab.utils import configclass -from isaaclab.utils.datasets import HDF5DatasetFileHandler +from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 from isaaclab_tasks.utils.parse_cfg import parse_env_cfg is_paused = False current_action_index = 0 -subtask_indices = [] +marked_subtask_action_indices = [] +skip_episode = False def play_cb(): @@ -84,10 +97,15 @@ def pause_cb(): is_paused = True +def skip_episode_cb(): + global skip_episode + skip_episode = True + + def mark_subtask_cb(): - global current_action_index, subtask_indices - subtask_indices.append(current_action_index) - print(f"Marked subtask at action index: {current_action_index}") + global current_action_index, marked_subtask_action_indices + marked_subtask_action_indices.append(current_action_index) + print(f"Marked a subtask signal at action index: {current_action_index}") class PreStepDatagenInfoRecorder(RecorderTerm): @@ -96,7 +114,7 @@ class PreStepDatagenInfoRecorder(RecorderTerm): def record_pre_step(self): eef_pose_dict = {} for eef_name in self._env.cfg.subtask_configs.keys(): - eef_pose_dict[eef_name] = self._env.get_robot_eef_pose(eef_name) + eef_pose_dict[eef_name] = self._env.get_robot_eef_pose(eef_name=eef_name) datagen_info = { "object_pose": self._env.get_object_poses(), @@ -113,6 +131,20 @@ class PreStepDatagenInfoRecorderCfg(RecorderTermCfg): class_type: type[RecorderTerm] = PreStepDatagenInfoRecorder +class PreStepSubtaskStartsObservationsRecorder(RecorderTerm): + """Recorder term that records the subtask start observations in each step.""" + + def record_pre_step(self): + return "obs/datagen_info/subtask_start_signals", self._env.get_subtask_start_signals() + + +@configclass +class PreStepSubtaskStartsObservationsRecorderCfg(RecorderTermCfg): + """Configuration for the subtask start observations recorder term.""" + + class_type: type[RecorderTerm] = PreStepSubtaskStartsObservationsRecorder + + class PreStepSubtaskTermsObservationsRecorder(RecorderTerm): """Recorder term that records the subtask completion observations in each step.""" @@ -132,16 +164,13 @@ class MimicRecorderManagerCfg(ActionStateRecorderManagerCfg): """Mimic specific recorder terms.""" record_pre_step_datagen_info = PreStepDatagenInfoRecorderCfg() + record_pre_step_subtask_start_signals = PreStepSubtaskStartsObservationsRecorderCfg() record_pre_step_subtask_term_signals = PreStepSubtaskTermsObservationsRecorderCfg() def main(): """Add Isaac Lab Mimic annotations to the given demo dataset file.""" - global is_paused, current_action_index, subtask_indices - - if not args_cli.auto and len(args_cli.signals) == 0: - if len(args_cli.signals) == 0: - raise ValueError("Subtask signals should be provided for manual mode.") + global is_paused, current_action_index, marked_subtask_action_indices # Load input dataset to be annotated if not os.path.exists(args_cli.input_file): @@ -153,7 +182,7 @@ def main(): if episode_count == 0: print("No episodes found in the dataset.") - exit() + return 0 # get output directory path and file name (without extension) from cli arguments output_dir = os.path.dirname(args_cli.output_file) @@ -163,13 +192,13 @@ def main(): os.makedirs(output_dir) if args_cli.task is not None: - env_name = args_cli.task + env_name = args_cli.task.split(":")[-1] if env_name is None: raise ValueError("Task/env name was not specified nor found in the dataset.") env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=1) - env_cfg.env_name = args_cli.task + env_cfg.env_name = env_name # extract success checking function to invoke manually success_term = None @@ -183,35 +212,73 @@ def main(): env_cfg.terminations = None # Set up recorder terms for mimic annotations - env_cfg.recorders: MimicRecorderManagerCfg = MimicRecorderManagerCfg() + env_cfg.recorders = MimicRecorderManagerCfg() if not args_cli.auto: # disable subtask term signals recorder term if in manual mode env_cfg.recorders.record_pre_step_subtask_term_signals = None + + if not args_cli.auto or (args_cli.auto and not args_cli.annotate_subtask_start_signals): + # disable subtask start signals recorder term if in manual mode or no need for subtask start annotations + env_cfg.recorders.record_pre_step_subtask_start_signals = None + env_cfg.recorders.dataset_export_dir_path = output_dir env_cfg.recorders.dataset_filename = output_file_name # create environment from loaded config - env = gym.make(args_cli.task, cfg=env_cfg).unwrapped + env: ManagerBasedRLMimicEnv = gym.make(args_cli.task, cfg=env_cfg).unwrapped - if not isinstance(env.unwrapped, ManagerBasedRLMimicEnv): + if not isinstance(env, ManagerBasedRLMimicEnv): raise ValueError("The environment should be derived from ManagerBasedRLMimicEnv") if args_cli.auto: - # check if the mimic API env.unwrapped.get_subtask_term_signals() is implemented - if env.unwrapped.get_subtask_term_signals.__func__ is ManagerBasedRLMimicEnv.get_subtask_term_signals: + # check if the mimic API env.get_subtask_term_signals() is implemented + if env.get_subtask_term_signals.__func__ is ManagerBasedRLMimicEnv.get_subtask_term_signals: raise NotImplementedError( "The environment does not implement the get_subtask_term_signals method required " "to run automatic annotations." ) + if ( + args_cli.annotate_subtask_start_signals + and env.get_subtask_start_signals.__func__ is ManagerBasedRLMimicEnv.get_subtask_start_signals + ): + raise NotImplementedError( + "The environment does not implement the get_subtask_start_signals method required " + "to run automatic annotations." + ) + else: + # get subtask termination signal names for each eef from the environment configs + subtask_term_signal_names = {} + subtask_start_signal_names = {} + for eef_name, eef_subtask_configs in env.cfg.subtask_configs.items(): + subtask_start_signal_names[eef_name] = ( + [subtask_config.subtask_term_signal for subtask_config in eef_subtask_configs] + if args_cli.annotate_subtask_start_signals + else [] + ) + subtask_term_signal_names[eef_name] = [ + subtask_config.subtask_term_signal for subtask_config in eef_subtask_configs + ] + # Validation: if annotating start signals, every subtask (including the last) must have a name + if args_cli.annotate_subtask_start_signals: + if any(name in (None, "") for name in subtask_start_signal_names[eef_name]): + raise ValueError( + f"Missing 'subtask_term_signal' for one or more subtasks in eef '{eef_name}'. When" + " '--annotate_subtask_start_signals' is enabled, each subtask (including the last) must" + " specify 'subtask_term_signal'. The last subtask's term signal name is used as the final" + " start signal name." + ) + # no need to annotate the last subtask term signal, so remove it from the list + subtask_term_signal_names[eef_name].pop() # reset environment env.reset() # Only enables inputs if this script is NOT headless mode if not args_cli.headless and not os.environ.get("HEADLESS", 0): - keyboard_interface = Se3Keyboard(pos_sensitivity=0.1, rot_sensitivity=0.1) + keyboard_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1)) keyboard_interface.add_callback("N", play_cb) keyboard_interface.add_callback("B", pause_cb) + keyboard_interface.add_callback("Q", skip_episode_cb) if not args_cli.auto: keyboard_interface.add_callback("S", mark_subtask_cb) keyboard_interface.reset() @@ -219,73 +286,31 @@ def main(): # simulate environment -- run everything in inference mode exported_episode_count = 0 processed_episode_count = 0 + successful_task_count = 0 # Counter for successful task completions with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): while simulation_app.is_running() and not simulation_app.is_exiting(): # Iterate over the episodes in the loaded dataset file for episode_index, episode_name in enumerate(dataset_file_handler.get_episode_names()): processed_episode_count += 1 - subtask_indices = [] print(f"\nAnnotating episode #{episode_index} ({episode_name})") - episode = dataset_file_handler.load_episode(episode_name, env.unwrapped.device) - episode_data = episode.data - - # read initial state from the loaded episode - initial_state = episode_data["initial_state"] - env.unwrapped.recorder_manager.reset() - env.unwrapped.reset_to(initial_state, None, is_relative=True) - - # replay actions from this episode - actions = episode_data["actions"] - first_action = True - for action_index, action in enumerate(actions): - current_action_index = action_index - if first_action: - first_action = False - else: - while is_paused: - env.unwrapped.sim.render() - continue - action_tensor = torch.Tensor(action).reshape([1, action.shape[0]]) - env.step(torch.Tensor(action_tensor)) + episode = dataset_file_handler.load_episode(episode_name, env.device) is_episode_annotated_successfully = False - if not args_cli.auto: - print(f"\tSubtasks marked at action indices: {subtask_indices}") - if len(args_cli.signals) != len(subtask_indices): - raise ValueError( - f"Number of annotated subtask signals {len(subtask_indices)} should be equal " - f" to number of subtasks {len(args_cli.signals)}" - ) - annotated_episode = env.unwrapped.recorder_manager.get_episode(0) - for subtask_index in range(len(args_cli.signals)): - # subtask termination signal is false until subtask is complete, and true afterwards - subtask_signals = torch.ones(len(actions), dtype=torch.bool) - subtask_signals[: subtask_indices[subtask_index]] = False - annotated_episode.add( - f"obs/datagen_info/subtask_term_signals/{args_cli.signals[subtask_index]}", subtask_signals - ) - is_episode_annotated_successfully = True + if args_cli.auto: + is_episode_annotated_successfully = annotate_episode_in_auto_mode(env, episode, success_term) else: - # check if all the subtask term signals are annotated - annotated_episode = env.unwrapped.recorder_manager.get_episode(0) - subtask_term_signal_dict = annotated_episode.data["obs"]["datagen_info"]["subtask_term_signals"] - is_episode_annotated_successfully = True - for signal_name, signal_flags in subtask_term_signal_dict.items(): - if not torch.any(signal_flags): - is_episode_annotated_successfully = False - print(f'\tDid not detect completion for the subtask "{signal_name}".') - - if not bool(success_term.func(env, **success_term.params)[0]): - is_episode_annotated_successfully = False - print("\tThe final task was not completed.") + is_episode_annotated_successfully = annotate_episode_in_manual_mode( + env, episode, success_term, subtask_term_signal_names, subtask_start_signal_names + ) - if is_episode_annotated_successfully: + if is_episode_annotated_successfully and not skip_episode: # set success to the recorded episode data and export to file - env.unwrapped.recorder_manager.set_success_to_episodes( - None, torch.tensor([[True]], dtype=torch.bool, device=env.unwrapped.device) + env.recorder_manager.set_success_to_episodes( + None, torch.tensor([[True]], dtype=torch.bool, device=env.device) ) - env.unwrapped.recorder_manager.export_episodes() + env.recorder_manager.export_episodes() exported_episode_count += 1 + successful_task_count += 1 # Increment successful task counter print("\tExported the annotated episode.") else: print("\tSkipped exporting the episode due to incomplete subtask annotations.") @@ -295,14 +320,219 @@ def main(): f"\nExported {exported_episode_count} (out of {processed_episode_count}) annotated" f" episode{'s' if exported_episode_count > 1 else ''}." ) + print( + f"Successful task completions: {successful_task_count}" + ) # This line is used by the dataset generation test case to check if the expected number of demos were annotated print("Exiting the app.") # Close environment after annotation is complete env.close() + return successful_task_count + + +def replay_episode( + env: ManagerBasedRLMimicEnv, + episode: EpisodeData, + success_term: TerminationTermCfg | None = None, +) -> bool: + """Replays an episode in the environment. + + This function replays the given recorded episode in the environment. It can optionally check if the task + was successfully completed using a success termination condition input. + + Args: + env: The environment to replay the episode in. + episode: The recorded episode data to replay. + success_term: Optional termination term to check for task success. + + Returns: + True if the episode was successfully replayed and the success condition was met (if provided), + False otherwise. + """ + global current_action_index, skip_episode, is_paused + # read initial state and actions from the loaded episode + initial_state = episode.data["initial_state"] + actions = episode.data["actions"] + env.sim.reset() + env.recorder_manager.reset() + env.reset_to(initial_state, None, is_relative=True) + first_action = True + for action_index, action in enumerate(actions): + current_action_index = action_index + if first_action: + first_action = False + else: + while is_paused or skip_episode: + env.sim.render() + if skip_episode: + return False + continue + action_tensor = torch.Tensor(action).reshape([1, action.shape[0]]) + env.step(torch.Tensor(action_tensor)) + if success_term is not None: + if not bool(success_term.func(env, **success_term.params)[0]): + return False + return True + + +def annotate_episode_in_auto_mode( + env: ManagerBasedRLMimicEnv, + episode: EpisodeData, + success_term: TerminationTermCfg | None = None, +) -> bool: + """Annotates an episode in automatic mode. + + This function replays the given episode in the environment and checks if the task was successfully completed. + If the task was not completed, it will print a message and return False. Otherwise, it will check if all the + subtask term signals are annotated and return True if they are, False otherwise. + + Args: + env: The environment to replay the episode in. + episode: The recorded episode data to replay. + success_term: Optional termination term to check for task success. + + Returns: + True if the episode was successfully annotated, False otherwise. + """ + global skip_episode + skip_episode = False + is_episode_annotated_successfully = replay_episode(env, episode, success_term) + if skip_episode: + print("\tSkipping the episode.") + return False + if not is_episode_annotated_successfully: + print("\tThe final task was not completed.") + else: + # check if all the subtask term signals are annotated + annotated_episode = env.recorder_manager.get_episode(0) + subtask_term_signal_dict = annotated_episode.data["obs"]["datagen_info"]["subtask_term_signals"] + for signal_name, signal_flags in subtask_term_signal_dict.items(): + signal_flags = torch.tensor(signal_flags, device=env.device) + if not torch.any(signal_flags): + is_episode_annotated_successfully = False + print(f'\tDid not detect completion for the subtask "{signal_name}".') + if args_cli.annotate_subtask_start_signals: + subtask_start_signal_dict = annotated_episode.data["obs"]["datagen_info"]["subtask_start_signals"] + for signal_name, signal_flags in subtask_start_signal_dict.items(): + if not torch.any(signal_flags): + is_episode_annotated_successfully = False + print(f'\tDid not detect start for the subtask "{signal_name}".') + return is_episode_annotated_successfully + + +def annotate_episode_in_manual_mode( + env: ManagerBasedRLMimicEnv, + episode: EpisodeData, + success_term: TerminationTermCfg | None = None, + subtask_term_signal_names: dict[str, list[str]] = {}, + subtask_start_signal_names: dict[str, list[str]] = {}, +) -> bool: + """Annotates an episode in manual mode. + + This function replays the given episode in the environment and allows for manual marking of subtask term signals. + It iterates over each eef and prompts the user to mark the subtask term signals for that eef. + + Args: + env: The environment to replay the episode in. + episode: The recorded episode data to replay. + success_term: Optional termination term to check for task success. + subtask_term_signal_names: Dictionary mapping eef names to lists of subtask term signal names. + subtask_start_signal_names: Dictionary mapping eef names to lists of subtask start signal names. + Returns: + True if the episode was successfully annotated, False otherwise. + """ + global is_paused, marked_subtask_action_indices, skip_episode + # iterate over the eefs for marking subtask term signals + subtask_term_signal_action_indices = {} + subtask_start_signal_action_indices = {} + for eef_name, eef_subtask_term_signal_names in subtask_term_signal_names.items(): + eef_subtask_start_signal_names = subtask_start_signal_names[eef_name] + # skip if no subtask annotation is needed for this eef + if len(eef_subtask_term_signal_names) == 0 and len(eef_subtask_start_signal_names) == 0: + continue + + while True: + is_paused = True + skip_episode = False + print(f'\tPlaying the episode for subtask annotations for eef "{eef_name}".') + print("\tSubtask signals to annotate:") + if len(eef_subtask_start_signal_names) > 0: + print(f"\t\t- Start:\t{eef_subtask_start_signal_names}") + print(f"\t\t- Termination:\t{eef_subtask_term_signal_names}") + + print('\n\tPress "N" to begin.') + print('\tPress "B" to pause.') + print('\tPress "S" to annotate subtask signals.') + print('\tPress "Q" to skip the episode.\n') + marked_subtask_action_indices = [] + task_success_result = replay_episode(env, episode, success_term) + if skip_episode: + print("\tSkipping the episode.") + return False + + print(f"\tSubtasks marked at action indices: {marked_subtask_action_indices}") + expected_subtask_signal_count = len(eef_subtask_term_signal_names) + len(eef_subtask_start_signal_names) + if task_success_result and expected_subtask_signal_count == len(marked_subtask_action_indices): + print(f'\tAll {expected_subtask_signal_count} subtask signals for eef "{eef_name}" were annotated.') + for marked_signal_index in range(expected_subtask_signal_count): + if args_cli.annotate_subtask_start_signals and marked_signal_index % 2 == 0: + subtask_start_signal_action_indices[ + eef_subtask_start_signal_names[int(marked_signal_index / 2)] + ] = marked_subtask_action_indices[marked_signal_index] + if not args_cli.annotate_subtask_start_signals: + # Direct mapping when only collecting termination signals + subtask_term_signal_action_indices[eef_subtask_term_signal_names[marked_signal_index]] = ( + marked_subtask_action_indices[marked_signal_index] + ) + elif args_cli.annotate_subtask_start_signals and marked_signal_index % 2 == 1: + # Every other signal is a termination when collecting both types + subtask_term_signal_action_indices[ + eef_subtask_term_signal_names[math.floor(marked_signal_index / 2)] + ] = marked_subtask_action_indices[marked_signal_index] + break + + if not task_success_result: + print("\tThe final task was not completed.") + return False + + if expected_subtask_signal_count != len(marked_subtask_action_indices): + print( + f"\tOnly {len(marked_subtask_action_indices)} out of" + f' {expected_subtask_signal_count} subtask signals for eef "{eef_name}" were' + " annotated." + ) + + print(f'\tThe episode will be replayed again for re-marking subtask signals for the eef "{eef_name}".\n') + + annotated_episode = env.recorder_manager.get_episode(0) + for ( + subtask_term_signal_name, + subtask_term_signal_action_index, + ) in subtask_term_signal_action_indices.items(): + # subtask termination signal is false until subtask is complete, and true afterwards + subtask_signals = torch.ones(len(episode.data["actions"]), dtype=torch.bool) + subtask_signals[:subtask_term_signal_action_index] = False + annotated_episode.add(f"obs/datagen_info/subtask_term_signals/{subtask_term_signal_name}", subtask_signals) + + if args_cli.annotate_subtask_start_signals: + for ( + subtask_start_signal_name, + subtask_start_signal_action_index, + ) in subtask_start_signal_action_indices.items(): + subtask_signals = torch.ones(len(episode.data["actions"]), dtype=torch.bool) + subtask_signals[:subtask_start_signal_action_index] = False + annotated_episode.add( + f"obs/datagen_info/subtask_start_signals/{subtask_start_signal_name}", subtask_signals + ) + + return True + if __name__ == "__main__": # run the main function - main() + successful_task_count = main() # close sim app simulation_app.close() + # exit with the number of successful task completions as return code + exit(successful_task_count) diff --git a/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py b/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py index 456517d..57381af 100644 --- a/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py +++ b/scripts/imitation_learning/isaaclab_mimic/consolidated_demo.py @@ -1,12 +1,7 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 """ Script to record teleoperated demos and run mimic dataset generation in real-time. @@ -85,7 +80,7 @@ import time import torch -from isaaclab.devices import Se3Keyboard, Se3SpaceMouse +from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg, Se3SpaceMouse, Se3SpaceMouseCfg from isaaclab.envs import ManagerBasedRLMimicEnv from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg from isaaclab.managers import DatasetExportMode, RecorderTerm, RecorderTermCfg @@ -97,6 +92,7 @@ from isaaclab_mimic.datagen.datagen_info_pool import DataGenInfoPool import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 from isaaclab_tasks.utils.parse_cfg import parse_env_cfg # global variable to keep track of the data generation statistics @@ -203,9 +199,9 @@ async def run_teleop_robot( # create controller if needed if teleop_interface is None: if args_cli.teleop_device.lower() == "keyboard": - teleop_interface = Se3Keyboard(pos_sensitivity=0.2, rot_sensitivity=0.5) + teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) elif args_cli.teleop_device.lower() == "spacemouse": - teleop_interface = Se3SpaceMouse(pos_sensitivity=0.2, rot_sensitivity=0.5) + teleop_interface = Se3SpaceMouse(Se3SpaceMouseCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) else: raise ValueError( f"Invalid device interface '{args_cli.teleop_device}'. Supported: 'keyboard', 'spacemouse'." @@ -306,6 +302,7 @@ 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 @@ -365,7 +362,7 @@ def main(): # get the environment name if args_cli.task is not None: - env_name = args_cli.task + env_name = args_cli.task.split(":")[-1] elif args_cli.input_file: # if the environment name is not specified, try to get it from the dataset file dataset_file_handler = HDF5DatasetFileHandler() @@ -405,7 +402,7 @@ def main(): env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_SUCCEEDED_ONLY # create environment - env = gym.make(env_name, cfg=env_cfg) + env = gym.make(args_cli.task, cfg=env_cfg) if not isinstance(env.unwrapped, ManagerBasedRLMimicEnv): raise ValueError("The environment should be derived from ManagerBasedRLMimicEnv") diff --git a/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py b/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py index 612d7d3..fcc85f8 100644 --- a/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py +++ b/scripts/imitation_learning/isaaclab_mimic/generate_dataset.py @@ -1,18 +1,14 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 """ Main data generation script. """ -# Launching Isaac Sim Simulator first. + +"""Launch Isaac Sim Simulator first.""" import argparse @@ -37,11 +33,28 @@ action="store_true", help="pause after every subtask during generation for debugging - only useful with render flag", ) +parser.add_argument( + "--enable_pinocchio", + action="store_true", + default=False, + help="Enable Pinocchio.", +) +parser.add_argument( + "--use_skillgen", + action="store_true", + default=False, + help="use skillgen to generate motion trajectories", +) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments args_cli = parser.parse_args() +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim + # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter + import pinocchio # noqa: F401 + # launch the simulator app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -50,15 +63,27 @@ import asyncio import gymnasium as gym +import inspect +import logging import numpy as np import random import torch +from isaaclab.envs import ManagerBasedRLMimicEnv + import isaaclab_mimic.envs # noqa: F401 + +if args_cli.enable_pinocchio: + import isaaclab_mimic.envs.pinocchio_envs # noqa: F401 + from isaaclab_mimic.datagen.generation import env_loop, setup_async_generation, setup_env_config from isaaclab_mimic.datagen.utils import get_env_name_from_dataset, setup_output_paths import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 + +# import logger +logger = logging.getLogger(__name__) def main(): @@ -66,7 +91,10 @@ def main(): # Setup output paths and get env name output_dir, output_file_name = setup_output_paths(args_cli.output_file) - env_name = args_cli.task or get_env_name_from_dataset(args_cli.input_file) + task_name = args_cli.task + if task_name: + task_name = args_cli.task.split(":")[-1] + env_name = task_name or get_env_name_from_dataset(args_cli.input_file) # Configure environment env_cfg, success_term = setup_env_config( @@ -78,17 +106,55 @@ def main(): generation_num_trials=args_cli.generation_num_trials, ) - # create environment - env = gym.make(env_name, cfg=env_cfg) + # Create environment + env = gym.make(env_name, cfg=env_cfg).unwrapped + + if not isinstance(env, ManagerBasedRLMimicEnv): + raise ValueError("The environment should be derived from ManagerBasedRLMimicEnv") - # set seed for generation - random.seed(env.unwrapped.cfg.datagen_config.seed) - np.random.seed(env.unwrapped.cfg.datagen_config.seed) - torch.manual_seed(env.unwrapped.cfg.datagen_config.seed) + # 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: + 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." + ) - # reset before starting + # Set seed for generation + random.seed(env.cfg.datagen_config.seed) + np.random.seed(env.cfg.datagen_config.seed) + torch.manual_seed(env.cfg.datagen_config.seed) + + # Reset before starting env.reset() + motion_planners = None + if args_cli.use_skillgen: + from isaaclab_mimic.motion_planners.curobo.curobo_planner import CuroboPlanner + from isaaclab_mimic.motion_planners.curobo.curobo_planner_cfg import CuroboPlannerCfg + + # Create one motion planner per environment + motion_planners = {} + for env_id in range(num_envs): + print(f"Initializing motion planner for environment {env_id}") + # Create a config instance from the task name + planner_config = CuroboPlannerCfg.from_task_name(env_name) + + # Ensure visualization is only enabled for the first environment + # If not, sphere and plan visualization will be too slow in isaac lab + # It is efficient to visualize the spheres and plan for the first environment in rerun + if env_id != 0: + planner_config.visualize_spheres = False + planner_config.visualize_plan = False + + motion_planners[env_id] = CuroboPlanner( + env=env, + robot=env.scene["robot"], + config=planner_config, # Pass the config object + env_id=env_id, # Pass environment ID + ) + + env.cfg.datagen_config.use_skillgen = True + # Setup and run async data generation async_components = setup_async_generation( env=env, @@ -96,13 +162,38 @@ def main(): input_file=args_cli.input_file, success_term=success_term, pause_subtask=args_cli.pause_subtask, + motion_planners=motion_planners, # Pass the motion planners dictionary ) try: - asyncio.ensure_future(asyncio.gather(*async_components["tasks"])) - env_loop(env, async_components["action_queue"], async_components["info_pool"], async_components["event_loop"]) + data_gen_tasks = asyncio.ensure_future(asyncio.gather(*async_components["tasks"])) + env_loop( + env, + async_components["reset_queue"], + async_components["action_queue"], + async_components["info_pool"], + async_components["event_loop"], + ) except asyncio.CancelledError: print("Tasks were cancelled.") + finally: + # Cancel all async tasks when env_loop finishes + data_gen_tasks.cancel() + try: + # Wait for tasks to be cancelled + async_components["event_loop"].run_until_complete(data_gen_tasks) + except asyncio.CancelledError: + print("Remaining async tasks cancelled and cleaned up.") + except Exception as e: + print(f"Error cancelling remaining async tasks: {e}") + # Cleanup of motion planners and their visualizers + if motion_planners is not None: + for env_id, planner in motion_planners.items(): + if getattr(planner, "plan_visualizer", None) is not None: + print(f"Closing plan visualizer for environment {env_id}") + planner.plan_visualizer.close() + planner.plan_visualizer = None + motion_planners.clear() if __name__ == "__main__": @@ -110,5 +201,5 @@ def main(): main() except KeyboardInterrupt: print("\nProgram interrupted by user. Exiting...") - # close sim app + # Close sim app simulation_app.close() diff --git a/scripts/imitation_learning/locomanipulation_sdg/generate_data.py b/scripts/imitation_learning/locomanipulation_sdg/generate_data.py new file mode 100644 index 0000000..f9ed832 --- /dev/null +++ b/scripts/imitation_learning/locomanipulation_sdg/generate_data.py @@ -0,0 +1,774 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to replay demonstrations with Isaac Lab environments.""" + +"""Launch Isaac Sim Simulator first.""" + + +import argparse +import os + +from isaaclab.app import AppLauncher + +# Launch Isaac Lab +parser = argparse.ArgumentParser(description="Locomanipulation SDG") +parser.add_argument("--task", type=str, help="The Isaac Lab locomanipulation SDG task to load for data generation.") +parser.add_argument("--dataset", type=str, help="The static manipulation dataset recorded via teleoperation.") +parser.add_argument("--output_file", type=str, help="The file name for the generated output dataset.") +parser.add_argument( + "--lift_step", + type=int, + help=( + "The step index in the input recording where the robot is ready to lift the object. Aka, where the grasp is" + " finished." + ), +) +parser.add_argument( + "--navigate_step", + type=int, + help=( + "The step index in the input recording where the robot is ready to navigate. Aka, where it has finished" + " lifting the object" + ), +) +parser.add_argument("--demo", type=str, default=None, help="The demo in the input dataset to use.") +parser.add_argument("--num_runs", type=int, default=1, help="The number of trajectories to generate.") +parser.add_argument( + "--draw_visualization", type=bool, default=False, help="Draw the occupancy map and path planning visualization." +) +parser.add_argument( + "--angular_gain", + type=float, + default=2.0, + help=( + "The angular gain to use for determining an angular control velocity when driving the robot during navigation." + ), +) +parser.add_argument( + "--linear_gain", + type=float, + default=1.0, + help="The linear gain to use for determining the linear control velocity when driving the robot during navigation.", +) +parser.add_argument( + "--linear_max", type=float, default=1.0, help="The maximum linear control velocity allowable during navigation." +) +parser.add_argument( + "--distance_threshold", + type=float, + default=0.2, + help="The distance threshold in meters to perform state transitions between navigation and manipulation tasks.", +) +parser.add_argument( + "--following_offset", + type=float, + default=0.6, + help=( + "The target point offset distance used for local path following during navigation. A larger value will result" + " in smoother trajectories, but may cut path corners." + ), +) +parser.add_argument( + "--angle_threshold", + type=float, + default=0.2, + help=( + "The angle threshold in radians to determine when the robot can move forward or transition between navigation" + " and manipulation tasks." + ), +) +parser.add_argument( + "--approach_distance", + type=float, + default=0.5, + help="An offset distance added to the destination to allow a buffer zone for reliably approaching the goal.", +) +parser.add_argument( + "--randomize_placement", + type=bool, + default=True, + help="Whether or not to randomize the placement of fixtures in the scene upon environment initialization.", +) +parser.add_argument( + "--enable_pinocchio", + action="store_true", + default=False, + help="Enable Pinocchio.", +) +AppLauncher.add_app_launcher_args(parser) +args_cli = parser.parse_args() + +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim + # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter + import pinocchio # noqa: F401 + +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +import enum +import gymnasium as gym +import random +import torch + +import omni.kit + +from isaaclab.utils import configclass +from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler + +import isaaclab_mimic.locomanipulation_sdg.envs # noqa: F401 +from isaaclab_mimic.locomanipulation_sdg.data_classes import LocomanipulationSDGOutputData +from isaaclab_mimic.locomanipulation_sdg.envs.locomanipulation_sdg_env import LocomanipulationSDGEnv +from isaaclab_mimic.locomanipulation_sdg.occupancy_map_utils import ( + OccupancyMap, + merge_occupancy_maps, + occupancy_map_add_to_stage, +) +from isaaclab_mimic.locomanipulation_sdg.path_utils import ParameterizedPath, plan_path +from isaaclab_mimic.locomanipulation_sdg.scene_utils import RelativePose, place_randomly +from isaaclab_mimic.locomanipulation_sdg.transform_utils import transform_inv, transform_mul, transform_relative_pose + +from isaaclab_tasks.utils import parse_env_cfg + + +class LocomanipulationSDGDataGenerationState(enum.IntEnum): + """States for the locomanipulation SDG data generation state machine.""" + + GRASP_OBJECT = 0 + """Robot grasps object at start position""" + + LIFT_OBJECT = 1 + """Robot lifts object while stationary""" + + NAVIGATE = 2 + """Robot navigates to approach position with object""" + + APPROACH = 3 + """Robot approaches final goal position""" + + DROP_OFF_OBJECT = 4 + """Robot places object at end position""" + + DONE = 5 + """Task completed""" + + +@configclass +class LocomanipulationSDGControlConfig: + """Configuration for navigation control parameters.""" + + angular_gain: float = 2.0 + """Proportional gain for angular velocity control""" + + linear_gain: float = 1.0 + """Proportional gain for linear velocity control""" + + linear_max: float = 1.0 + """Maximum allowed linear velocity (m/s)""" + + distance_threshold: float = 0.1 + """Distance threshold for state transitions (m)""" + + following_offset: float = 0.6 + """Look-ahead distance for path following (m)""" + + angle_threshold: float = 0.2 + """Angular threshold for orientation control (rad)""" + + approach_distance: float = 1.0 + """Buffer distance from final goal (m)""" + + +def compute_navigation_velocity( + current_pose: torch.Tensor, target_xy: torch.Tensor, config: LocomanipulationSDGControlConfig +) -> tuple[torch.Tensor, torch.Tensor]: + """Compute linear and angular velocities for navigation control. + + Args: + current_pose: Current robot pose [x, y, yaw] + target_xy: Target position [x, y] + config: Navigation control configuration + + Returns: + Tuple of (linear_velocity, angular_velocity) + """ + current_xy = current_pose[:2] + current_yaw = current_pose[2] + + # Compute position and orientation errors + delta_xy = target_xy - current_xy + delta_distance = torch.sqrt(torch.sum(delta_xy**2)) + + target_yaw = torch.arctan2(delta_xy[1], delta_xy[0]) + delta_yaw = target_yaw - current_yaw + # Normalize angle to [-Ο€, Ο€] + delta_yaw = (delta_yaw + torch.pi) % (2 * torch.pi) - torch.pi + + # Compute control commands + angular_velocity = config.angular_gain * delta_yaw + linear_velocity = torch.clip(config.linear_gain * delta_distance, 0.0, config.linear_max) / ( + 1 + torch.abs(angular_velocity) + ) + + return linear_velocity, angular_velocity + + +def load_and_transform_recording_data( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + recording_step: int, + reference_pose: torch.Tensor, + target_pose: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + """Load recording data and transform hand targets to current reference frame. + + Args: + env: The locomanipulation SDG environment + input_episode_data: Input episode data from static manipulation + recording_step: Current step in the recording + reference_pose: Original reference pose for the hand targets + target_pose: Current target pose to transform to + + Returns: + Tuple of transformed (left_hand_pose, right_hand_pose) + """ + recording_item = env.load_input_data(input_episode_data, recording_step) + if recording_item is None: + return None, None + + left_hand_pose = transform_relative_pose(recording_item.left_hand_pose_target, reference_pose, target_pose)[0] + right_hand_pose = transform_relative_pose(recording_item.right_hand_pose_target, reference_pose, target_pose)[0] + + return left_hand_pose, right_hand_pose + + +def setup_navigation_scene( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + approach_distance: float, + randomize_placement: bool = True, +) -> tuple[OccupancyMap, ParameterizedPath, RelativePose, RelativePose]: + """Set up the navigation scene with occupancy map and path planning. + + Args: + env: The locomanipulation SDG environment + input_episode_data: Input episode data + approach_distance: Buffer distance from final goal + randomize_placement: Whether to randomize fixture placement + + Returns: + 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(), + ]) + + # Randomize fixture placement if enabled + if randomize_placement: + fixtures = [env.get_end_fixture()] + env.get_obstacle_fixtures() + for fixture in fixtures: + place_randomly(fixture, occupancy_map.buffered_meters(1.0)) + occupancy_map = merge_occupancy_maps([occupancy_map, fixture.get_occupancy_map()]) + + # Compute goal poses from initial state + initial_state = env.load_input_data(input_episode_data, 0) + base_goal = RelativePose( + relative_pose=transform_mul(transform_inv(initial_state.fixture_pose), initial_state.base_pose), + parent=env.get_end_fixture(), + ) + base_goal_approach = RelativePose( + relative_pose=torch.tensor([-approach_distance, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]), parent=base_goal + ) + + # Plan navigation path + base_path = plan_path( + start=env.get_base(), end=base_goal_approach, occupancy_map=occupancy_map.buffered_meters(0.15) + ) + base_path_helper = ParameterizedPath(base_path) + + return occupancy_map, base_path_helper, base_goal, base_goal_approach + + +def handle_grasp_state( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + recording_step: int, + lift_step: int, + output_data: LocomanipulationSDGOutputData, +) -> tuple[int, LocomanipulationSDGDataGenerationState]: + """Handle the GRASP_OBJECT state logic. + + Args: + env: The environment + input_episode_data: Input episode data + recording_step: Current recording step + lift_step: Step to transition to lift phase + output_data: Output data to populate + + Returns: + Tuple of (next_recording_step, next_state) + """ + recording_item = env.load_input_data(input_episode_data, recording_step) + + # Set control targets - robot stays stationary during grasping + output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.GRASP_OBJECT) + output_data.recording_step = recording_step + output_data.base_velocity_target = torch.tensor([0.0, 0.0, 0.0]) + + # Transform hand poses relative to object + output_data.left_hand_pose_target = transform_relative_pose( + recording_item.left_hand_pose_target, recording_item.object_pose, env.get_object().get_pose() + )[0] + output_data.right_hand_pose_target = transform_relative_pose( + recording_item.right_hand_pose_target, recording_item.base_pose, env.get_base().get_pose() + )[0] + output_data.left_hand_joint_positions_target = recording_item.left_hand_joint_positions_target + output_data.right_hand_joint_positions_target = recording_item.right_hand_joint_positions_target + + # Update state + + next_recording_step = recording_step + 1 + next_state = ( + LocomanipulationSDGDataGenerationState.LIFT_OBJECT + if next_recording_step > lift_step + else LocomanipulationSDGDataGenerationState.GRASP_OBJECT + ) + + return next_recording_step, next_state + + +def handle_lift_state( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + recording_step: int, + navigate_step: int, + output_data: LocomanipulationSDGOutputData, +) -> tuple[int, LocomanipulationSDGDataGenerationState]: + """Handle the LIFT_OBJECT state logic. + + Args: + env: The environment + input_episode_data: Input episode data + recording_step: Current recording step + navigate_step: Step to transition to navigation phase + output_data: Output data to populate + + Returns: + Tuple of (next_recording_step, next_state) + """ + recording_item = env.load_input_data(input_episode_data, recording_step) + + # Set control targets - robot stays stationary during lifting + output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.LIFT_OBJECT) + output_data.recording_step = recording_step + output_data.base_velocity_target = torch.tensor([0.0, 0.0, 0.0]) + + # Transform hand poses relative to base + output_data.left_hand_pose_target = transform_relative_pose( + recording_item.left_hand_pose_target, recording_item.base_pose, env.get_base().get_pose() + )[0] + output_data.right_hand_pose_target = transform_relative_pose( + recording_item.right_hand_pose_target, recording_item.object_pose, env.get_object().get_pose() + )[0] + output_data.left_hand_joint_positions_target = recording_item.left_hand_joint_positions_target + output_data.right_hand_joint_positions_target = recording_item.right_hand_joint_positions_target + + # Update state + next_recording_step = recording_step + 1 + next_state = ( + LocomanipulationSDGDataGenerationState.NAVIGATE + if next_recording_step > navigate_step + else LocomanipulationSDGDataGenerationState.LIFT_OBJECT + ) + + return next_recording_step, next_state + + +def handle_navigate_state( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + recording_step: int, + base_path_helper: ParameterizedPath, + base_goal_approach: RelativePose, + config: LocomanipulationSDGControlConfig, + output_data: LocomanipulationSDGOutputData, +) -> LocomanipulationSDGDataGenerationState: + """Handle the NAVIGATE state logic. + + Args: + env: The environment + input_episode_data: Input episode data + recording_step: Current recording step + base_path_helper: Parameterized path for navigation + base_goal_approach: Approach pose goal + config: Navigation control configuration + output_data: Output data to populate + + Returns: + Next state + """ + recording_item = env.load_input_data(input_episode_data, recording_step) + current_pose = env.get_base().get_pose_2d()[0] + + # Find target point along path using pure pursuit algorithm + _, nearest_path_length, _, _ = base_path_helper.find_nearest(current_pose[:2]) + target_xy = base_path_helper.get_point_by_distance(distance=nearest_path_length + config.following_offset) + + # Compute navigation velocities + linear_velocity, angular_velocity = compute_navigation_velocity(current_pose, target_xy, config) + + # Set control targets + output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.NAVIGATE) + output_data.recording_step = recording_step + output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity]) + + # Transform hand poses relative to base + output_data.left_hand_pose_target = transform_relative_pose( + recording_item.left_hand_pose_target, recording_item.base_pose, env.get_base().get_pose() + )[0] + output_data.right_hand_pose_target = transform_relative_pose( + recording_item.right_hand_pose_target, recording_item.base_pose, env.get_base().get_pose() + )[0] + output_data.left_hand_joint_positions_target = recording_item.left_hand_joint_positions_target + output_data.right_hand_joint_positions_target = recording_item.right_hand_joint_positions_target + + # Check if close enough to approach goal to transition + goal_xy = base_goal_approach.get_pose_2d()[0, :2] + distance_to_goal = torch.sqrt(torch.sum((current_pose[:2] - goal_xy) ** 2)) + + return ( + LocomanipulationSDGDataGenerationState.APPROACH + if distance_to_goal < config.distance_threshold + else LocomanipulationSDGDataGenerationState.NAVIGATE + ) + + +def handle_approach_state( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + recording_step: int, + base_goal: RelativePose, + config: LocomanipulationSDGControlConfig, + output_data: LocomanipulationSDGOutputData, +) -> LocomanipulationSDGDataGenerationState: + """Handle the APPROACH state logic. + + Args: + env: The environment + input_episode_data: Input episode data + recording_step: Current recording step + base_goal: Final goal pose + config: Navigation control configuration + output_data: Output data to populate + + Returns: + Next state + """ + recording_item = env.load_input_data(input_episode_data, recording_step) + current_pose = env.get_base().get_pose_2d()[0] + + # Navigate directly to final goal position + goal_xy = base_goal.get_pose_2d()[0, :2] + linear_velocity, angular_velocity = compute_navigation_velocity(current_pose, goal_xy, config) + + # Set control targets + output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.APPROACH) + output_data.recording_step = recording_step + output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity]) + + # Transform hand poses relative to base + output_data.left_hand_pose_target = transform_relative_pose( + recording_item.left_hand_pose_target, recording_item.base_pose, env.get_base().get_pose() + )[0] + output_data.right_hand_pose_target = transform_relative_pose( + recording_item.right_hand_pose_target, recording_item.base_pose, env.get_base().get_pose() + )[0] + output_data.left_hand_joint_positions_target = recording_item.left_hand_joint_positions_target + output_data.right_hand_joint_positions_target = recording_item.right_hand_joint_positions_target + + # Check if close enough to final goal to start drop-off + distance_to_goal = torch.sqrt(torch.sum((current_pose[:2] - goal_xy) ** 2)) + + return ( + LocomanipulationSDGDataGenerationState.DROP_OFF_OBJECT + if distance_to_goal < config.distance_threshold + else LocomanipulationSDGDataGenerationState.APPROACH + ) + + +def handle_drop_off_state( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + recording_step: int, + base_goal: RelativePose, + config: LocomanipulationSDGControlConfig, + output_data: LocomanipulationSDGOutputData, +) -> tuple[int, LocomanipulationSDGDataGenerationState | None]: + """Handle the DROP_OFF_OBJECT state logic. + + Args: + env: The environment + input_episode_data: Input episode data + recording_step: Current recording step + base_goal: Final goal pose + config: Navigation control configuration + output_data: Output data to populate + + Returns: + Tuple of (next_recording_step, next_state) + """ + recording_item = env.load_input_data(input_episode_data, recording_step) + if recording_item is None: + return recording_step, None + + # Compute orientation control to face target orientation + current_pose = env.get_base().get_pose_2d()[0] + target_pose = base_goal.get_pose_2d()[0] + current_yaw = current_pose[2] + target_yaw = target_pose[2] + delta_yaw = target_yaw - current_yaw + delta_yaw = (delta_yaw + torch.pi) % (2 * torch.pi) - torch.pi + + angular_velocity = config.angular_gain * delta_yaw + linear_velocity = 0.0 # Stay in place while orienting + + # Set control targets + output_data.data_generation_state = int(LocomanipulationSDGDataGenerationState.DROP_OFF_OBJECT) + output_data.recording_step = recording_step + output_data.base_velocity_target = torch.tensor([linear_velocity, 0.0, angular_velocity]) + + # Transform hand poses relative to end fixture + output_data.left_hand_pose_target = transform_relative_pose( + recording_item.left_hand_pose_target, + recording_item.fixture_pose, + env.get_end_fixture().get_pose(), + )[0] + output_data.right_hand_pose_target = transform_relative_pose( + recording_item.right_hand_pose_target, + recording_item.fixture_pose, + env.get_end_fixture().get_pose(), + )[0] + output_data.left_hand_joint_positions_target = recording_item.left_hand_joint_positions_target + output_data.right_hand_joint_positions_target = recording_item.right_hand_joint_positions_target + + # Continue playback if orientation is within threshold + next_recording_step = recording_step + 1 if abs(delta_yaw) < config.angle_threshold else recording_step + + return next_recording_step, LocomanipulationSDGDataGenerationState.DROP_OFF_OBJECT + + +def populate_output_data( + env: LocomanipulationSDGEnv, + output_data: LocomanipulationSDGOutputData, + base_goal: RelativePose, + base_goal_approach: RelativePose, + base_path: torch.Tensor, +) -> None: + """Populate remaining output data fields. + + Args: + env: The environment + output_data: Output data to populate + base_goal: Final goal pose + base_goal_approach: Approach goal pose + base_path: Planned navigation path + """ + output_data.base_pose = env.get_base().get_pose() + output_data.object_pose = env.get_object().get_pose() + output_data.start_fixture_pose = env.get_start_fixture().get_pose() + output_data.end_fixture_pose = env.get_end_fixture().get_pose() + output_data.base_goal_pose = base_goal.get_pose() + output_data.base_goal_approach_pose = base_goal_approach.get_pose() + output_data.base_path = base_path + + # Collect obstacle poses + obstacle_poses = [] + for obstacle in env.get_obstacle_fixtures(): + obstacle_poses.append(obstacle.get_pose()) + if obstacle_poses: + output_data.obstacle_fixture_poses = torch.cat(obstacle_poses, dim=0)[None, :] + else: + output_data.obstacle_fixture_poses = torch.empty((1, 0, 7)) # Empty tensor with correct shape + + +def replay( + env: LocomanipulationSDGEnv, + input_episode_data: EpisodeData, + lift_step: int, + navigate_step: int, + draw_visualization: bool = False, + angular_gain: float = 2.0, + linear_gain: float = 1.0, + linear_max: float = 1.0, + distance_threshold: float = 0.1, + following_offset: float = 0.6, + angle_threshold: float = 0.2, + approach_distance: float = 1.0, + randomize_placement: bool = True, +) -> None: + """Replay a locomanipulation SDG episode with state machine control. + + This function implements a state machine for locomanipulation SDG, where the robot: + 1. Grasps an object at the start position + 2. Lifts the object while stationary + 3. Navigates with the object to an approach position + 4. Approaches the final goal position + 5. Places the object at the end position + + Args: + env: The locomanipulation SDG environment + input_episode_data: Static manipulation episode data to replay + lift_step: Recording step where lifting phase begins + navigate_step: Recording step where navigation phase begins + draw_visualization: Whether to visualize occupancy map and path + angular_gain: Proportional gain for angular velocity control + linear_gain: Proportional gain for linear velocity control + linear_max: Maximum linear velocity (m/s) + distance_threshold: Distance threshold for state transitions (m) + following_offset: Look-ahead distance for path following (m) + angle_threshold: Angular threshold for orientation control (rad) + approach_distance: Buffer distance from final goal (m) + randomize_placement: Whether to randomize obstacle placement + """ + + # Initialize environment to starting state + env.reset_to(state=input_episode_data.get_initial_state(), env_ids=torch.tensor([0]), is_relative=True) + + # Create navigation control configuration + config = LocomanipulationSDGControlConfig( + angular_gain=angular_gain, + linear_gain=linear_gain, + linear_max=linear_max, + distance_threshold=distance_threshold, + following_offset=following_offset, + angle_threshold=angle_threshold, + approach_distance=approach_distance, + ) + + # Set up navigation scene and path planning + occupancy_map, base_path_helper, base_goal, base_goal_approach = setup_navigation_scene( + env, input_episode_data, approach_distance, randomize_placement + ) + + # Visualize occupancy map and path if requested + if draw_visualization: + occupancy_map_add_to_stage( + occupancy_map, + stage=omni.usd.get_context().get_stage(), + path="/OccupancyMap", + z_offset=0.01, + draw_path=base_path_helper.points, + ) + + # Initialize state machine + output_data = LocomanipulationSDGOutputData() + current_state = LocomanipulationSDGDataGenerationState.GRASP_OBJECT + recording_step = 0 + + # Main simulation loop with state machine + while simulation_app.is_running() and not simulation_app.is_exiting(): + + print(f"Current state: {current_state.name}, Recording step: {recording_step}") + + # Execute state-specific logic using helper functions + if current_state == LocomanipulationSDGDataGenerationState.GRASP_OBJECT: + recording_step, current_state = handle_grasp_state( + env, input_episode_data, recording_step, lift_step, output_data + ) + + elif current_state == LocomanipulationSDGDataGenerationState.LIFT_OBJECT: + recording_step, current_state = handle_lift_state( + env, input_episode_data, recording_step, navigate_step, output_data + ) + + elif current_state == LocomanipulationSDGDataGenerationState.NAVIGATE: + current_state = handle_navigate_state( + env, input_episode_data, recording_step, base_path_helper, base_goal_approach, config, output_data + ) + + elif current_state == LocomanipulationSDGDataGenerationState.APPROACH: + current_state = handle_approach_state( + env, input_episode_data, recording_step, base_goal, config, output_data + ) + + elif current_state == LocomanipulationSDGDataGenerationState.DROP_OFF_OBJECT: + recording_step, next_state = handle_drop_off_state( + env, input_episode_data, recording_step, base_goal, config, output_data + ) + if next_state is None: # End of episode data + break + current_state = next_state + + # Populate additional output data fields + populate_output_data(env, output_data, base_goal, base_goal_approach, base_path_helper.points) + + # Attach output data to environment for recording + env._locomanipulation_sdg_output_data = output_data + + # Build and execute action + action = env.build_action_vector( + base_velocity_target=output_data.base_velocity_target, + left_hand_joint_positions_target=output_data.left_hand_joint_positions_target, + right_hand_joint_positions_target=output_data.right_hand_joint_positions_target, + left_hand_pose_target=output_data.left_hand_pose_target, + right_hand_pose_target=output_data.right_hand_pose_target, + ) + + env.step(action) + + +if __name__ == "__main__": + + with torch.no_grad(): + + # Create environment + if args_cli.task is not None: + env_name = args_cli.task.split(":")[-1] + if env_name is None: + raise ValueError("Task/env name was not specified nor found in the dataset.") + + env_cfg = parse_env_cfg(env_name, device=args_cli.device, num_envs=1) + env_cfg.sim.device = "cpu" + env_cfg.recorders.dataset_export_dir_path = os.path.dirname(args_cli.output_file) + env_cfg.recorders.dataset_filename = os.path.basename(args_cli.output_file) + + env = gym.make(args_cli.task, cfg=env_cfg).unwrapped + + # Load input data + input_dataset_file_handler = HDF5DatasetFileHandler() + input_dataset_file_handler.open(args_cli.dataset) + + 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: + demo = args_cli.demo + + input_episode_data = input_dataset_file_handler.load_episode(demo, args_cli.device) + + replay( + env=env, + input_episode_data=input_episode_data, + lift_step=args_cli.lift_step, + navigate_step=args_cli.navigate_step, + draw_visualization=args_cli.draw_visualization, + angular_gain=args_cli.angular_gain, + linear_gain=args_cli.linear_gain, + linear_max=args_cli.linear_max, + distance_threshold=args_cli.distance_threshold, + following_offset=args_cli.following_offset, + angle_threshold=args_cli.angle_threshold, + approach_distance=args_cli.approach_distance, + randomize_placement=args_cli.randomize_placement, + ) + + env.reset() # FIXME: hack to handle missing final recording + env.close() + + simulation_app.close() diff --git a/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py b/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py new file mode 100644 index 0000000..706b2d6 --- /dev/null +++ b/scripts/imitation_learning/locomanipulation_sdg/plot_navigation_trajectory.py @@ -0,0 +1,109 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to visualize navigation datasets. + +Loads a navigation dataset and generates plots showing paths, poses and obstacles. + +Args: + dataset: Path to the HDF5 dataset file containing recorded demonstrations. + output_dir: Directory path where visualization plots will be saved. + figure_size: Size of the generated figures (width, height). + demo_filter: If provided, only visualize specific demo(s). Can be a single demo name or comma-separated list. +""" + +import argparse +import h5py +import matplotlib.pyplot as plt +import os + + +def main(): + """Main function to process dataset and generate visualizations.""" + # add argparse arguments + parser = argparse.ArgumentParser( + description="Visualize navigation dataset from locomanipulation sdg demonstrations." + ) + parser.add_argument( + "--input_file", type=str, help="Path to the HDF5 dataset file containing recorded demonstrations." + ) + parser.add_argument("--output_dir", type=str, help="Directory path where visualization plots will be saved.") + parser.add_argument( + "--figure_size", + type=int, + nargs=2, + default=[20, 20], + help="Size of the generated figures (width, height). Default: [20, 20]", + ) + parser.add_argument( + "--demo_filter", + type=str, + default=None, + help="If provided, only visualize specific demo(s). Can be a single demo name or comma-separated list.", + ) + + # parse the arguments + args = parser.parse_args() + + # Validate inputs + if not os.path.exists(args.input_file): + raise FileNotFoundError(f"Dataset file not found: {args.input_file}") + + # Create output directory if it doesn't exist + os.makedirs(args.output_dir, exist_ok=True) + + # Load dataset + dataset = h5py.File(args.input_file, "r") + + demos = list(dataset["data"].keys()) + + # Filter demos if specified + if args.demo_filter: + filter_demos = [d.strip() for d in args.demo_filter.split(",")] + demos = [d for d in demos if d in filter_demos] + if not demos: + print(f"Warning: No demos found matching filter '{args.demo_filter}'") + return + + print(f"Visualizing {len(demos)} demonstrations...") + + for i, demo in enumerate(demos): + print(f"Processing demo {i + 1}/{len(demos)}: {demo}") + + replay_data = dataset["data"][demo]["locomanipulation_sdg_output_data"] + path = replay_data["base_path"] + base_pose = replay_data["base_pose"] + object_pose = replay_data["object_pose"] + start_pose = replay_data["start_fixture_pose"] + end_pose = replay_data["end_fixture_pose"] + obstacle_poses = replay_data["obstacle_fixture_poses"] + + plt.figure(figsize=args.figure_size) + plt.plot(path[0, :, 0], path[0, :, 1], "r-", label="Target Path", linewidth=2) + plt.plot(base_pose[:, 0], base_pose[:, 1], "g--", label="Base Pose", linewidth=2) + plt.plot(object_pose[:, 0], object_pose[:, 1], "b--", label="Object Pose", linewidth=2) + plt.plot(obstacle_poses[0, :, 0], obstacle_poses[0, :, 1], "ro", label="Obstacles", markersize=8) + + # Add start and end markers + plt.plot(start_pose[0, 0], start_pose[0, 1], "gs", label="Start", markersize=12) + plt.plot(end_pose[0, 0], end_pose[0, 1], "rs", label="End", markersize=12) + + plt.legend(loc="upper right", ncol=1, fontsize=12) + plt.axis("equal") + plt.grid(True, alpha=0.3) + plt.title(f"Navigation Visualization - {demo}", fontsize=16) + plt.xlabel("X Position (m)", fontsize=14) + plt.ylabel("Y Position (m)", fontsize=14) + + output_path = os.path.join(args.output_dir, f"{demo}.png") + plt.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close() # Close the figure to free memory + + dataset.close() + print(f"Visualization complete! Plots saved to: {args.output_dir}") + + +if __name__ == "__main__": + main() diff --git a/scripts/imitation_learning/robomimic/play.py b/scripts/imitation_learning/robomimic/play.py index 7ba816a..77b7d2e 100644 --- a/scripts/imitation_learning/robomimic/play.py +++ b/scripts/imitation_learning/robomimic/play.py @@ -1,17 +1,25 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -"""Script to play and evaluate a trained policy from robomimic.""" +"""Script to play and evaluate a trained policy from robomimic. + +This script loads a robomimic policy and plays it in an Isaac Lab environment. + +Args: + task: Name of the environment. + checkpoint: Path to the robomimic policy checkpoint. + horizon: If provided, override the step horizon of each rollout. + num_rollouts: If provided, override the number of rollouts. + seed: If provided, overeride the default random seed. + norm_factor_min: If provided, minimum value of the action space normalization factor. + norm_factor_max: If provided, maximum value of the action space normalization factor. +""" """Launch Isaac Sim Simulator first.""" + import argparse from isaaclab.app import AppLauncher @@ -26,41 +34,93 @@ parser.add_argument("--horizon", type=int, default=800, help="Step horizon of each rollout.") parser.add_argument("--num_rollouts", type=int, default=1, help="Number of rollouts.") parser.add_argument("--seed", type=int, default=101, help="Random seed.") +parser.add_argument( + "--norm_factor_min", type=float, default=None, help="Optional: minimum value of the normalization factor." +) +parser.add_argument( + "--norm_factor_max", type=float, default=None, help="Optional: maximum value of the normalization factor." +) +parser.add_argument("--enable_pinocchio", default=False, action="store_true", help="Enable Pinocchio.") + # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments args_cli = parser.parse_args() +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim + # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter + import pinocchio # noqa: F401 + # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything follows.""" +import copy import gymnasium as gym +import numpy as np +import random import torch import robomimic.utils.file_utils as FileUtils import robomimic.utils.torch_utils as TorchUtils +if args_cli.enable_pinocchio: + import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 + import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 + from isaaclab_tasks.utils import parse_env_cfg -def rollout(policy, env, horizon, device): - policy.start_episode +def rollout(policy, env, success_term, horizon, device): + """Perform a single rollout of the policy in the environment. + + Args: + policy: The robomimicpolicy to play. + env: The environment to play in. + horizon: The step horizon of each rollout. + device: The device to run the policy on. + + Returns: + terminated: Whether the rollout terminated. + traj: The trajectory of the rollout. + """ + policy.start_episode() obs_dict, _ = env.reset() traj = dict(actions=[], obs=[], next_obs=[]) for i in range(horizon): # Prepare observations - obs = obs_dict["policy"] + obs = copy.deepcopy(obs_dict["policy"]) for ob in obs: obs[ob] = torch.squeeze(obs[ob]) + + # Check if environment image observations + if hasattr(env.cfg, "image_obs_list"): + # Process image observations for robomimic inference + for image_name in env.cfg.image_obs_list: + if image_name in obs_dict["policy"].keys(): + # Convert from chw uint8 to hwc normalized float + image = torch.squeeze(obs_dict["policy"][image_name]) + image = image.permute(2, 0, 1).clone().float() + image = image / 255.0 + image = image.clip(0.0, 1.0) + obs[image_name] = image + traj["obs"].append(obs) # Compute actions actions = policy(obs) + + # Unnormalize actions + if args_cli.norm_factor_min is not None and args_cli.norm_factor_max is not None: + actions = ( + (actions + 1) * (args_cli.norm_factor_max - args_cli.norm_factor_min) + ) / 2 + args_cli.norm_factor_min + actions = torch.from_numpy(actions).to(device=device).view(1, env.action_space.shape[1]) # Apply actions @@ -71,9 +131,10 @@ def rollout(policy, env, horizon, device): traj["actions"].append(actions.tolist()) traj["next_obs"].append(obs) - if terminated: + # Check if rollout was successful + if bool(success_term.func(env, **success_term.params)[0]): return True, traj - elif truncated: + elif terminated or truncated: return False, traj return False, traj @@ -93,24 +154,28 @@ def main(): # Disable recorder env_cfg.recorders = None + # Extract success checking function + success_term = env_cfg.terminations.success + env_cfg.terminations.success = None + # Create environment env = gym.make(args_cli.task, cfg=env_cfg).unwrapped # Set seed torch.manual_seed(args_cli.seed) + np.random.seed(args_cli.seed) + random.seed(args_cli.seed) env.seed(args_cli.seed) # Acquire device device = TorchUtils.get_torch_device(try_to_use_cuda=True) - # Load policy - policy, _ = FileUtils.policy_from_checkpoint(ckpt_path=args_cli.checkpoint, device=device, verbose=True) - # Run policy results = [] for trial in range(args_cli.num_rollouts): print(f"[INFO] Starting trial {trial}") - terminated, traj = rollout(policy, env, args_cli.horizon, device) + policy, _ = FileUtils.policy_from_checkpoint(ckpt_path=args_cli.checkpoint, device=device) + terminated, traj = rollout(policy, env, success_term, args_cli.horizon, device) results.append(terminated) print(f"[INFO] Trial {trial}: {terminated}\n") diff --git a/scripts/imitation_learning/robomimic/robust_eval.py b/scripts/imitation_learning/robomimic/robust_eval.py new file mode 100644 index 0000000..1f93d41 --- /dev/null +++ b/scripts/imitation_learning/robomimic/robust_eval.py @@ -0,0 +1,334 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to evaluate a trained policy from robomimic across multiple evaluation settings. + +This script loads a trained robomimic policy and evaluates it in an Isaac Lab environment +across multiple evaluation settings (lighting, textures, etc.) and seeds. It saves the results +to a specified output directory. + +Args: + task: Name of the environment. + input_dir: Directory containing the model checkpoints to evaluate. + horizon: Step horizon of each rollout. + num_rollouts: Number of rollouts per model per setting. + num_seeds: Number of random seeds to evaluate. + seeds: Optional list of specific seeds to use instead of random ones. + log_dir: Directory to write results to. + log_file: Name of the output file. + output_vis_file: File path to export recorded episodes. + norm_factor_min: If provided, minimum value of the action space normalization factor. + norm_factor_max: If provided, maximum value of the action space normalization factor. + disable_fabric: Whether to disable fabric and use USD I/O operations. +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Evaluate robomimic policy for Isaac Lab environment.") +parser.add_argument( + "--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations." +) +parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument("--input_dir", type=str, default=None, help="Directory containing models to evaluate.") +parser.add_argument( + "--start_epoch", type=int, default=100, help="Epoch of the checkpoint to start the evaluation from." +) +parser.add_argument("--horizon", type=int, default=400, help="Step horizon of each rollout.") +parser.add_argument("--num_rollouts", type=int, default=15, help="Number of rollouts for each setting.") +parser.add_argument("--num_seeds", type=int, default=3, help="Number of random seeds to evaluate.") +parser.add_argument("--seeds", nargs="+", type=int, default=None, help="List of specific seeds to use.") +parser.add_argument( + "--log_dir", type=str, default="/tmp/policy_evaluation_results", help="Directory to write results to." +) +parser.add_argument("--log_file", type=str, default="results", help="Name of output file.") +parser.add_argument( + "--output_vis_file", type=str, default="visuals.hdf5", help="File path to export recorded episodes." +) +parser.add_argument( + "--norm_factor_min", type=float, default=None, help="Optional: minimum value of the normalization factor." +) +parser.add_argument( + "--norm_factor_max", type=float, default=None, help="Optional: maximum value of the normalization factor." +) +parser.add_argument("--enable_pinocchio", default=False, action="store_true", help="Enable Pinocchio.") + +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim + # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter + import pinocchio # noqa: F401 + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import copy +import gymnasium as gym +import os +import pathlib +import random +import torch + +import robomimic.utils.file_utils as FileUtils +import robomimic.utils.torch_utils as TorchUtils + +from isaaclab_tasks.utils import parse_env_cfg + + +def rollout(policy, env: gym.Env, success_term, horizon: int, device: torch.device) -> tuple[bool, dict]: + """Perform a single rollout of the policy in the environment. + + Args: + policy: The robomimic policy to evaluate. + env: The environment to evaluate in. + horizon: The step horizon of each rollout. + device: The device to run the policy on. + args_cli: Command line arguments containing normalization factors. + + Returns: + terminated: Whether the rollout terminated successfully. + traj: The trajectory of the rollout. + """ + policy.start_episode() + obs_dict, _ = env.reset() + traj = dict(actions=[], obs=[], next_obs=[]) + + for _ in range(horizon): + # Prepare policy observations + obs = copy.deepcopy(obs_dict["policy"]) + for ob in obs: + obs[ob] = torch.squeeze(obs[ob]) + + # Check if environment image observations + if hasattr(env.cfg, "image_obs_list"): + # Process image observations for robomimic inference + for image_name in env.cfg.image_obs_list: + if image_name in obs_dict["policy"].keys(): + # Convert from chw uint8 to hwc normalized float + image = torch.squeeze(obs_dict["policy"][image_name]) + image = image.permute(2, 0, 1).clone().float() + image = image / 255.0 + image = image.clip(0.0, 1.0) + obs[image_name] = image + + traj["obs"].append(obs) + + # Compute actions + actions = policy(obs) + + # Unnormalize actions if normalization factors are provided + if args_cli.norm_factor_min is not None and args_cli.norm_factor_max is not None: + actions = ( + (actions + 1) * (args_cli.norm_factor_max - args_cli.norm_factor_min) + ) / 2 + args_cli.norm_factor_min + + actions = torch.from_numpy(actions).to(device=device).view(1, env.action_space.shape[1]) + + # Apply actions + obs_dict, _, terminated, truncated, _ = env.step(actions) + obs = obs_dict["policy"] + + # Record trajectory + traj["actions"].append(actions.tolist()) + traj["next_obs"].append(obs) + + if bool(success_term.func(env, **success_term.params)[0]): + return True, traj + elif terminated or truncated: + return False, traj + + return False, traj + + +def evaluate_model( + model_path: str, + env: gym.Env, + device: torch.device, + success_term, + num_rollouts: int, + horizon: int, + seed: int, + output_file: str, +) -> float: + """Evaluate a single model checkpoint across multiple rollouts. + + Args: + model_path: Path to the model checkpoint. + env: The environment to evaluate in. + device: The device to run the policy on. + num_rollouts: Number of rollouts to perform. + horizon: Step horizon of each rollout. + seed: Random seed to use. + output_file: File to write results to. + + Returns: + float: Success rate of the model + """ + # Set seed + torch.manual_seed(seed) + env.seed(seed) + random.seed(seed) + + # Load policy + policy, _ = FileUtils.policy_from_checkpoint(ckpt_path=model_path, device=device, verbose=False) + + # Run policy + results = [] + for trial in range(num_rollouts): + print(f"[Model: {os.path.basename(model_path)}] Starting trial {trial}") + terminated, _ = rollout(policy, env, success_term, horizon, device) + results.append(terminated) + with open(output_file, "a") as file: + file.write(f"[Model: {os.path.basename(model_path)}] Trial {trial}: {terminated}\n") + print(f"[Model: {os.path.basename(model_path)}] Trial {trial}: {terminated}") + + # Calculate and log results + success_rate = results.count(True) / len(results) + with open(output_file, "a") as file: + file.write( + f"[Model: {os.path.basename(model_path)}] Successful trials: {results.count(True)}, out of" + f" {len(results)} trials\n" + ) + file.write(f"[Model: {os.path.basename(model_path)}] Success rate: {success_rate}\n") + file.write(f"[Model: {os.path.basename(model_path)}] Results: {results}\n") + file.write("-" * 80 + "\n\n") + + print( + f"\n[Model: {os.path.basename(model_path)}] Successful trials: {results.count(True)}, out of" + f" {len(results)} trials" + ) + print(f"[Model: {os.path.basename(model_path)}] Success rate: {success_rate}\n") + print(f"[Model: {os.path.basename(model_path)}] Results: {results}\n") + + return success_rate + + +def main() -> None: + """Run evaluation of trained policies from robomimic with Isaac Lab environment.""" + # Parse configuration + env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1, use_fabric=not args_cli.disable_fabric) + + # Set observations to dictionary mode for Robomimic + env_cfg.observations.policy.concatenate_terms = False + + # Set termination conditions + env_cfg.terminations.time_out = None + + # Disable recorder + env_cfg.recorders = None + + # Extract success checking function + success_term = env_cfg.terminations.success + env_cfg.terminations.success = None + + # Set evaluation settings + env_cfg.eval_mode = True + + # Create environment + env = gym.make(args_cli.task, cfg=env_cfg).unwrapped + + # Acquire device + device = TorchUtils.get_torch_device(try_to_use_cuda=False) + + # Get model checkpoints + model_checkpoints = [f.name for f in os.scandir(args_cli.input_dir) if f.is_file()] + + # Set up seeds + seeds = random.sample(range(0, 10000), args_cli.num_seeds) if args_cli.seeds is None else args_cli.seeds + + # Define evaluation settings + settings = ["vanilla", "light_intensity", "light_color", "light_texture", "table_texture", "robot_texture", "all"] + + # Create log directory if it doesn't exist + os.makedirs(args_cli.log_dir, exist_ok=True) + + # Evaluate each seed + for seed in seeds: + output_path = os.path.join(args_cli.log_dir, f"{args_cli.log_file}_seed_{seed}") + path = pathlib.Path(output_path) + path.parent.mkdir(parents=True, exist_ok=True) + + # Initialize results summary + results_summary = dict() + results_summary["overall"] = {} + for setting in settings: + results_summary[setting] = {} + + with open(output_path, "w") as file: + # Evaluate each setting + for setting in settings: + env.cfg.eval_type = setting + + file.write(f"Evaluation setting: {setting}\n") + file.write("=" * 80 + "\n\n") + + print(f"Evaluation setting: {setting}") + print("=" * 80) + + # Evaluate each model + for model in model_checkpoints: + # Skip early checkpoints + model_epoch = int(model.split(".")[0].split("_")[-1]) + if model_epoch < args_cli.start_epoch: + continue + + model_path = os.path.join(args_cli.input_dir, model) + success_rate = evaluate_model( + model_path=model_path, + env=env, + device=device, + success_term=success_term, + num_rollouts=args_cli.num_rollouts, + horizon=args_cli.horizon, + seed=seed, + output_file=output_path, + ) + + # Store results + results_summary[setting][model] = success_rate + if model not in results_summary["overall"].keys(): + results_summary["overall"][model] = 0.0 + results_summary["overall"][model] += success_rate + + env.reset() + + file.write("=" * 80 + "\n\n") + env.reset() + + # Calculate overall success rates + for model in results_summary["overall"].keys(): + results_summary["overall"][model] /= len(settings) + + # Write final summary + file.write("\nResults Summary (success rate):\n") + for setting in results_summary.keys(): + file.write(f"\nSetting: {setting}\n") + for model in results_summary[setting].keys(): + file.write(f"{model}: {results_summary[setting][model]}\n") + max_key = max(results_summary[setting], key=results_summary[setting].get) + file.write( + f"\nBest model for setting {setting} is {max_key} with success rate" + f" {results_summary[setting][max_key]}\n" + ) + + env.close() + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/imitation_learning/robomimic/train.py b/scripts/imitation_learning/robomimic/train.py index 47464bd..837a897 100644 --- a/scripts/imitation_learning/robomimic/train.py +++ b/scripts/imitation_learning/robomimic/train.py @@ -1,31 +1,7 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# MIT License -# -# Copyright (c) 2021 Stanford Vision and Learning Lab -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. @@ -33,14 +9,19 @@ """ The main entry point for training policies from pre-collected data. -Args: - algo: name of the algorithm to run. - task: name of the environment. - name: if provided, override the experiment name defined in the config - dataset: if provided, override the dataset path defined in the config - -This file has been modified from the original version in the following ways: +This script loads dataset(s), creates a model based on the algorithm specified, +and trains the model. It supports training on various environments with multiple +algorithms from robomimic. +Args: + algo: Name of the algorithm to run. + task: Name of the environment. + name: If provided, override the experiment name defined in the config. + dataset: If provided, override the dataset path defined in the config. + log_dir: Directory to save logs. + normalize_training_actions: Whether to normalize actions in the training data. + +This file has been modified from the original robomimic version to integrate with IsaacLab. """ """Launch Isaac Sim Simulator first.""" @@ -53,11 +34,17 @@ """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 @@ -66,21 +53,80 @@ from torch.utils.data import DataLoader 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 from robomimic.algo import algo_factory -from robomimic.config import config_factory +from robomimic.config import Config, config_factory from robomimic.utils.log_utils import DataLogger, PrintLogger -# Needed so that environment is registered +# Isaac Lab imports (needed so that environment is registered) import isaaclab_tasks # noqa: F401 - - -def train(config, device): - """Train a model using the algorithm.""" +import uwlab_tasks # noqa: F401 +import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 +import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 + + +def normalize_hdf5_actions(config: Config, log_dir: str) -> str: + """Normalizes actions in hdf5 dataset to [-1, 1] range. + + Args: + config: The configuration object containing dataset path. + log_dir: Directory to save normalization parameters. + + Returns: + Path to the normalized dataset. + """ + base, ext = os.path.splitext(config.train.data) + normalized_path = base + "_normalized" + ext + + # Copy the original dataset + print(f"Creating normalized dataset at {normalized_path}") + shutil.copyfile(config.train.data, normalized_path) + + # Open the new dataset and normalize the actions + with h5py.File(normalized_path, "r+") as f: + dataset_paths = [f"/data/demo_{str(i)}/actions" for i in range(len(f["data"].keys()))] + + # Compute the min and max of the dataset + dataset = np.array(f[dataset_paths[0]]).flatten() + for i, path in enumerate(dataset_paths): + if i != 0: + data = np.array(f[path]).flatten() + dataset = np.append(dataset, data) + + max = np.max(dataset) + min = np.min(dataset) + + # Normalize the actions + for i, path in enumerate(dataset_paths): + data = np.array(f[path]) + normalized_data = 2 * ((data - min) / (max - min)) - 1 # Scale to [-1, 1] range + del f[path] + f[path] = normalized_data + + # Save the min and max values to log directory + with open(os.path.join(log_dir, "normalization_params.txt"), "w") as f: + f.write(f"min: {min}\n") + f.write(f"max: {max}\n") + + return normalized_path + + +def train(config: Config, device: str, log_dir: str, ckpt_dir: str, video_dir: str): + """Train a model using the algorithm specified in config. + + Args: + config: Configuration object. + device: PyTorch device to use for training. + log_dir: Directory to save logs. + ckpt_dir: Directory to save checkpoints. + video_dir: Directory to save videos. + """ # first set seeds np.random.seed(config.train.seed) torch.manual_seed(config.train.seed) @@ -88,7 +134,6 @@ def train(config, device): print("\n============= New Training Run with Config =============") print(config) print("") - log_dir, ckpt_dir, video_dir = TrainUtils.get_exp_dir(config) print(f">>> Saving logs into directory: {log_dir}") print(f">>> Saving checkpoints into directory: {ckpt_dir}") @@ -224,7 +269,8 @@ def train(config, device): and (epoch % config.experiment.save.every_n_epochs == 0) ) epoch_list_check = epoch in config.experiment.save.epochs - should_save_ckpt = time_check or epoch_check or epoch_list_check + last_epoch_check = epoch == config.train.num_epochs + should_save_ckpt = time_check or epoch_check or epoch_list_check or last_epoch_check ckpt_reason = None if should_save_ckpt: last_ckpt_time = time.time() @@ -283,25 +329,41 @@ def train(config, device): data_logger.close() -def main(args): - """Train a model on a task using a specified algorithm.""" +def main(args: argparse.Namespace): + """Train a model on a task using a specified algorithm. + + Args: + args: Command line arguments. + """ # load config if args.task is not None: # obtain the configuration entry point cfg_entry_point_key = f"robomimic_{args.algo}_cfg_entry_point" + task_name = args.task.split(":")[-1] - print(f"Loading configuration for task: {args.task}") + print(f"Loading configuration for task: {task_name}") print(gym.envs.registry.keys()) print(" ") - cfg_entry_point_file = gym.spec(args.task).kwargs.pop(cfg_entry_point_key) + cfg_entry_point_file = gym.spec(task_name).kwargs.pop(cfg_entry_point_key) # check if entry point exists if cfg_entry_point_file is None: raise ValueError( - f"Could not find configuration for the environment: '{args.task}'." + f"Could not find configuration for the environment: '{task_name}'." 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 @@ -317,9 +379,17 @@ def main(args): if args.name is not None: config.experiment.name = args.name + if args.epochs is not None: + config.train.num_epochs = args.epochs + # change location of experiment directory config.train.output_dir = os.path.abspath(os.path.join("./logs", args.log_dir, args.task)) + log_dir, ckpt_dir, video_dir = TrainUtils.get_exp_dir(config) + + if args.normalize_training_actions: + config.train.data = normalize_hdf5_actions(config, log_dir) + # get torch device device = TorchUtils.get_torch_device(try_to_use_cuda=config.train.cuda) @@ -328,7 +398,7 @@ def main(args): # catch error during training and print it res_str = "finished run successfully!" try: - train(config, device=device) + train(config, device, log_dir, ckpt_dir, video_dir) except Exception as e: res_str = f"run failed with error:\n{e}\n\n{traceback.format_exc()}" print(res_str) @@ -356,6 +426,16 @@ def main(args): parser.add_argument("--task", type=str, default=None, help="Name of the task.") parser.add_argument("--algo", type=str, default=None, help="Name of the algorithm.") parser.add_argument("--log_dir", type=str, default="robomimic", help="Path to log directory") + parser.add_argument("--normalize_training_actions", action="store_true", default=False, help="Normalize actions") + parser.add_argument( + "--epochs", + type=int, + default=None, + help=( + "Optional: Number of training epochs. If specified, overrides the number of epochs from the JSON training" + " config." + ), + ) args = parser.parse_args() diff --git a/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py b/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py index 5e8221f..11c1aa3 100644 --- a/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py +++ b/scripts/reinforcement_learning/ray/grok_cluster_with_kubectl.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause 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 2ba3d79..6e8d9a0 100644 --- a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py +++ b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cartpole_cfg.py @@ -1,15 +1,10 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause - import pathlib import sys +from typing import Any # Allow for import of items from the ray workflow. CUR_DIR = pathlib.Path(__file__).parent @@ -18,6 +13,7 @@ import util import vision_cfg from ray import tune +from ray.tune.stopper import Stopper class CartpoleRGBNoTuneJobCfg(vision_cfg.CameraJobCfg): @@ -53,3 +49,21 @@ 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 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 20 <= iter 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 4931f74..b666553 100644 --- a/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py +++ b/scripts/reinforcement_learning/ray/hyperparameter_tuning/vision_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -90,14 +85,12 @@ 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 @@ -105,6 +98,7 @@ 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"]: @@ -146,14 +140,12 @@ 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 32fb845..43196d4 100644 --- a/scripts/reinforcement_learning/ray/launch.py +++ b/scripts/reinforcement_learning/ray/launch.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py b/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py index 2947386..4c932e3 100644 --- a/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py +++ b/scripts/reinforcement_learning/ray/mlflow_to_local_tensorboard.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -71,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 cdd4d32..75b8b53 100644 --- a/scripts/reinforcement_learning/ray/submit_job.py +++ b/scripts/reinforcement_learning/ray/submit_job.py @@ -1,20 +1,8 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -import argparse -import os -import time -from concurrent.futures import ThreadPoolExecutor - -from ray import job_submission - """ This script submits aggregate job(s) to cluster(s) described in a config file containing ``name: address: http://:`` on @@ -31,7 +19,11 @@ creates several individual jobs when started on a cluster. Alternatively, an aggregate job could be a :file:'../wrap_resources.py` resource-wrapped job, which may contain several individual sub-jobs separated by -the + delimiter. +the + delimiter. An aggregate job could also be a :file:`../task_runner.py` multi-task submission job, +where each sub-job and its resource requirements are defined in a YAML configuration file. +In this mode, :file:`../task_runner.py` will read the YAML file (via --task_cfg), and +submit all defined sub-tasks to the Ray cluster, supporting per-job resource specification and +real-time streaming of sub-job outputs. If there are more aggregate jobs than cluster(s), aggregate jobs will be submitted as clusters become available via the defined relation above. If there are less aggregate job(s) @@ -53,9 +45,21 @@ # Example: Submitting resource wrapped job 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. + python3 scripts/reinforcement_learning/ray/submit_job.py --aggregate_jobs task_runner.py --task_cfg tasks.yaml + # For all command line arguments python3 scripts/reinforcement_learning/ray/submit_job.py -h """ + +import argparse +import os +import time +from concurrent.futures import ThreadPoolExecutor + +from ray import job_submission + script_directory = os.path.dirname(os.path.abspath(__file__)) CONFIG = {"working_dir": script_directory, "executable": "/workspace/isaaclab/isaaclab.sh -p"} diff --git a/scripts/reinforcement_learning/ray/task_runner.py b/scripts/reinforcement_learning/ray/task_runner.py new file mode 100644 index 0000000..6ac3ef5 --- /dev/null +++ b/scripts/reinforcement_learning/ray/task_runner.py @@ -0,0 +1,230 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script dispatches one or more user-defined Python tasks to workers in a Ray cluster. +Each task, along with its resource requirements and execution parameters, is specified in a YAML configuration file. +Users may define the number of CPUs, GPUs, and the amount of memory to allocate per task via the config file. + +Key features: +------------- +- Fine-grained, per-task resource management via config fields (`num_gpus`, `num_cpus`, `memory`). +- Parallel execution of multiple tasks using available resources across the Ray cluster. +- Option to specify node affinity for tasks, e.g., by hostname, node ID, or any node. +- Optional batch (simultaneous) or independent scheduling of tasks. + +Task scheduling and distribution are handled via Ray’s built-in resource manager. + +YAML configuration fields: +-------------------------- +- `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. +- `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). + - `num_gpus`: Number of GPUs to allocate (float or string arithmetic, e.g., "2*2"). + - `num_cpus`: Number of CPUs to allocate (float or string). + - `memory`: Amount of RAM in bytes (int or string). + - `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` (str): Specific hostname to place the task on. + - `node_id` (str): Specific node ID to place the task on. + + +Typical usage: +--------------- + +.. code-block:: bash + + # Print help and argument details: + python task_runner.py -h + + # Submit tasks defined in a YAML file to the Ray cluster (auto-detects Ray head address): + python task_runner.py --task_cfg /path/to/tasks.yaml + +YAML configuration example-1: +--------------------------- +.. code-block:: yaml + + pip: ["xxx"] + py_modules: ["my_package/my_package"] + concurrent: false + tasks: + - name: "Isaac-Cartpole-v0" + py_args: "-m torch.distributed.run --nnodes=1 --nproc_per_node=2 --rdzv_endpoint=localhost:29501 /workspace/isaaclab/scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --max_iterations 200 --headless --distributed" + num_gpus: 2 + num_cpus: 10 + memory: 10737418240 + - name: "script need some dependencies" + py_args: "script.py --option arg" + num_gpus: 0 + num_cpus: 1 + memory: 10*1024*1024*1024 + +YAML configuration example-2: +--------------------------- +.. code-block:: yaml + + pip: ["xxx"] + py_modules: ["my_package/my_package"] + concurrent: true + tasks: + - name: "Isaac-Cartpole-v0-multi-node-train-1" + py_args: "-m torch.distributed.run --nproc_per_node=1 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 /workspace/isaaclab/scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed --max_iterations 1000" + num_gpus: 1 + num_cpus: 10 + memory: 10*1024*1024*1024 + node: + specific: "hostname" + hostname: "xxx" + - name: "Isaac-Cartpole-v0-multi-node-train-2" + py_args: "-m torch.distributed.run --nproc_per_node=1 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=x.x.x.x:5555 /workspace/isaaclab/scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless --distributed --max_iterations 1000" + num_gpus: 1 + num_cpus: 10 + memory: 10*1024*1024*1024 + node: + specific: "hostname" + hostname: "xxx" + +To stop all tasks early, press Ctrl+C; the script will cancel all running Ray tasks. +""" + +import argparse +import yaml +from datetime import datetime + +import util + + +def parse_args() -> argparse.Namespace: + """ + Parse command-line arguments for the Ray task runner. + + Returns: + argparse.Namespace: The 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. + """ + parser = argparse.ArgumentParser(description="Run tasks from a YAML config file.") + parser.add_argument("--task_cfg", type=str, required=True, help="Path to the YAML task file.") + parser.add_argument("--ray_address", type=str, default="auto", help="the Ray address.") + parser.add_argument( + "--test", + action="store_true", + help=( + "Run nvidia-smi test instead of the arbitrary job," + "can use as a sanity check prior to any jobs to check " + "that GPU resources are correctly isolated." + ), + ) + return parser.parse_args() + + +def parse_task_resource(task: dict) -> util.JobResource: + """ + Parse task resource requirements from the YAML configuration. + + Args: + task (dict): Dictionary representing a single task's configuration. + Keys may include `num_gpus`, `num_cpus`, and `memory`, each either + as a number or evaluatable string expression. + + Returns: + util.JobResource: Resource object with the parsed values. + """ + 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"] + if "num_cpus" in task: + resource.num_cpus = eval(task["num_cpus"]) if isinstance(task["num_cpus"], str) else task["num_cpus"] + if "memory" in task: + resource.memory = eval(task["memory"]) if isinstance(task["memory"], str) else task["memory"] + return resource + + +def run_tasks( + tasks: list[dict], args: argparse.Namespace, runtime_env: dict | None = None, concurrent: bool = False +) -> None: + """ + Submit tasks to the Ray cluster for execution. + + Args: + tasks (list[dict]): A list of task configuration dictionaries. + args (argparse.Namespace): Parsed command-line arguments. + runtime_env (dict | None): Ray runtime environment configuration containing: + - pip (list[str] | None): Additional pip packages to install. + - py_modules (list[str] | None): Python modules to include in the environment. + concurrent (bool): Whether to launch tasks simultaneously as a batch, + or independently as resources become available. + + Returns: + None + """ + job_objs = [] + util.ray_init(ray_address=args.ray_address, runtime_env=runtime_env, log_to_driver=False) + for task in tasks: + resource = parse_task_resource(task) + print(f"[INFO] Creating job {task['name']} with resource={resource}") + job = util.Job( + name=task["name"], + py_args=task["py_args"], + resources=resource, + node=util.JobNode( + specific=task.get("node", {}).get("specific"), + hostname=task.get("node", {}).get("hostname"), + node_id=task.get("node", {}).get("node_id"), + ), + ) + job_objs.append(job) + start = datetime.now() + print(f"[INFO] Creating {len(job_objs)} jobs at {start.strftime('%H:%M:%S.%f')} with runtime env={runtime_env}") + # submit jobs + util.submit_wrapped_jobs( + jobs=job_objs, + test_mode=args.test, + concurrent=concurrent, + ) + end = datetime.now() + print( + f"[INFO] All jobs completed at {end.strftime('%H:%M:%S.%f')}, took {(end - start).total_seconds():.2f} seconds." + ) + + +def main() -> None: + """ + Main entry point for the Ray task runner script. + + Reads the YAML task configuration file, parses CLI arguments, + and dispatches tasks to the Ray cluster. + + Returns: + None + """ + args = parse_args() + with open(args.task_cfg) as f: + config = yaml.safe_load(f) + tasks = config["tasks"] + runtime_env = { + "pip": None if not config.get("pip") else config["pip"], + "py_modules": None if not config.get("py_modules") else config["py_modules"], + } + concurrent = config.get("concurrent", False) + run_tasks( + tasks=tasks, + args=args, + runtime_env=runtime_env, + concurrent=concurrent, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/reinforcement_learning/ray/tuner.py b/scripts/reinforcement_learning/ray/tuner.py index c3768af..908aca1 100644 --- a/scripts/reinforcement_learning/ray/tuner.py +++ b/scripts/reinforcement_learning/ray/tuner.py @@ -1,24 +1,22 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause - import argparse import importlib.util import os +import random +import subprocess import sys -from time import sleep +from time import sleep, time import ray import util from ray import air, tune +from ray.tune import Callback 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, @@ -63,6 +61,9 @@ PYTHON_EXEC = "./isaaclab.sh -p" WORKFLOW = "scripts/reinforcement_learning/rl_games/train.py" NUM_WORKERS_PER_NODE = 1 # needed for local parallelism +PROCESS_RESPONSE_TIMEOUT = 200.0 # seconds to wait before killing the process when it stops responding +MAX_LINES_TO_SEARCH_EXPERIMENT_LOGS = 1000 # maximum number of lines to read from the training process logs +MAX_LOG_EXTRACTION_ERRORS = 10 # maximum allowed LogExtractionErrors before we abort the whole training class IsaacLabTuneTrainable(tune.Trainable): @@ -76,12 +77,13 @@ class IsaacLabTuneTrainable(tune.Trainable): def setup(self, config: dict) -> None: """Get the invocation command, return quick for easy scheduling.""" self.data = None + self.time_since_last_proc_response = 0.0 self.invoke_cmd = util.get_invocation_command_from_cfg(cfg=config, python_cmd=PYTHON_EXEC, workflow=WORKFLOW) print(f"[INFO]: Recovered invocation with {self.invoke_cmd}") self.experiment = None def reset_config(self, new_config: dict): - """Allow environments to be re-used by fetching a new invocation command""" + """Allow environments to be reused by fetching a new invocation command""" self.setup(new_config) return True @@ -90,12 +92,21 @@ def step(self) -> dict: # When including this as first step instead of setup, experiments get scheduled faster # Don't want to block the scheduler while the experiment spins up print(f"[INFO]: Invoking experiment as first step with {self.invoke_cmd}...") - experiment = util.execute_job( - self.invoke_cmd, - identifier_string="", - extract_experiment=True, - persistent_dir=BASE_DIR, - ) + try: + experiment = util.execute_job( + self.invoke_cmd, + identifier_string="", + extract_experiment=True, # Keep this as True to return a valid dictionary + persistent_dir=BASE_DIR, + max_lines_to_search_logs=MAX_LINES_TO_SEARCH_EXPERIMENT_LOGS, + max_time_to_search_logs=PROCESS_RESPONSE_TIMEOUT, + ) + except util.LogExtractionError: + self.data = { + "LOG_EXTRACTION_ERROR_STOPPER_FLAG": True, + "done": True, + } + return self.data self.experiment = experiment print(f"[INFO]: Tuner recovered experiment info {experiment}") self.proc = experiment["proc"] @@ -115,11 +126,35 @@ def step(self) -> dict: while data is None: data = util.load_tensorboard_logs(self.tensorboard_logdir) + proc_status = self.proc.poll() + if proc_status is not None: + break sleep(2) # Lazy report metrics to avoid performance overhead if self.data is not None: - while util._dicts_equal(data, self.data): + data_ = {k: v for k, v in data.items() if k != "done"} + self_data_ = {k: v for k, v in self.data.items() if k != "done"} + unresponsiveness_start_time = time() + while util._dicts_equal(data_, self_data_): + self.time_since_last_proc_response = time() - unresponsiveness_start_time data = util.load_tensorboard_logs(self.tensorboard_logdir) + data_ = {k: v for k, v in data.items() if k != "done"} + proc_status = self.proc.poll() + if proc_status is not None: + break + if self.time_since_last_proc_response > PROCESS_RESPONSE_TIMEOUT: + self.time_since_last_proc_response = 0.0 + print("[WARNING]: Training workflow process is not responding, terminating...") + self.proc.terminate() + try: + self.proc.wait(timeout=20) + except subprocess.TimeoutExpired: + print("[ERROR]: The process did not terminate within timeout duration.") + self.proc.kill() + self.proc.wait() + self.data = data + self.data["done"] = True + return self.data sleep(2) # Lazy report metrics to avoid performance overhead self.data = data @@ -138,13 +173,71 @@ def default_resource_request(self): ) -def invoke_tuning_run(cfg: dict, args: argparse.Namespace) -> None: +class LogExtractionErrorStopper(tune.Stopper): + """Stopper that stops all trials if multiple LogExtractionErrors occur. + + Args: + max_errors: The maximum number of LogExtractionErrors allowed before terminating the experiment. + """ + + def __init__(self, max_errors: int): + self.max_errors = max_errors + self.error_count = 0 + + def __call__(self, trial_id, result): + """Increments the error count if trial has encountered a LogExtractionError. + + It does not stop the trial based on the metrics, always returning False. + """ + if result.get("LOG_EXTRACTION_ERROR_STOPPER_FLAG", False): + self.error_count += 1 + print( + f"[ERROR]: Encountered LogExtractionError {self.error_count} times. " + f"Maximum allowed is {self.max_errors}." + ) + return False + + def stop_all(self): + """Returns true if number of LogExtractionErrors exceeds the maximum allowed, terminating the experiment.""" + if self.error_count > self.max_errors: + print("[FATAL]: Encountered LogExtractionError more than allowed, aborting entire tuning run... ") + return True + else: + return False + + +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, + 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. + stopper: Custom stopper, optional. """ # Allow for early exit os.environ["TUNE_DISABLE_STRICT_METRIC_CHECKING"] = "1" @@ -152,17 +245,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 @@ -172,15 +265,23 @@ 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 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=stoppers, ) elif args.run_mode == "remote": # MLFlow, to MLFlow server @@ -194,17 +295,21 @@ 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=stoppers, ) 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, param_space=cfg, tune_config=tune.TuneConfig( + metric=args.metric, + mode=args.mode, search_alg=repeat_search, num_samples=args.num_samples, reuse_actors=True, @@ -312,8 +417,45 @@ def __init__(self, cfg: dict): default=3, help="How many times to repeat each hyperparameter config.", ) + parser.add_argument( + "--process_response_timeout", + type=float, + default=PROCESS_RESPONSE_TIMEOUT, + help="Training workflow process response timeout.", + ) + parser.add_argument( + "--max_lines_to_search_experiment_logs", + type=float, + default=MAX_LINES_TO_SEARCH_EXPERIMENT_LOGS, + help="Max number of lines to search for experiment logs before terminating the training workflow process.", + ) + parser.add_argument( + "--max_log_extraction_errors", + type=float, + default=MAX_LOG_EXTRACTION_ERRORS, + help="Max number number of LogExtractionError failures before we abort the whole tuning run.", + ) + 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 + MAX_LINES_TO_SEARCH_EXPERIMENT_LOGS = int(args.max_lines_to_search_experiment_logs) + print( + "[INFO]: The max number of lines to search for experiment logs before (early) terminating the training " + f"workflow process is set to {MAX_LINES_TO_SEARCH_EXPERIMENT_LOGS}.\n" + "[INFO]: The process response timeout, used while updating tensorboard scalars and searching for " + f"experiment logs, is set to {PROCESS_RESPONSE_TIMEOUT} seconds." + ) + MAX_LOG_EXTRACTION_ERRORS = int(args.max_log_extraction_errors) + print( + "[INFO]: Max number of LogExtractionError failures before we abort the whole tuning run is " + f"set to {MAX_LOG_EXTRACTION_ERRORS}.\n" + ) NUM_WORKERS_PER_NODE = args.num_workers_per_node print(f"[INFO]: Using {NUM_WORKERS_PER_NODE} workers per node.") if args.run_mode == "remote": @@ -357,7 +499,16 @@ 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}'") + invoke_tuning_run(cfg, args, 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 9140ca9..c6eb79e 100644 --- a/scripts/reinforcement_learning/ray/util.py +++ b/scripts/reinforcement_learning/ray/util.py @@ -1,22 +1,23 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause - import argparse import os import re +import select import subprocess +import sys import threading +from collections.abc import Sequence +from dataclasses import dataclass from datetime import datetime from math import isclose +from time import time +from typing import Any import ray +from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy from tensorboard.backend.event_processing.directory_watcher import DirectoryDeletedError from tensorboard.backend.event_processing.event_accumulator import EventAccumulator @@ -32,6 +33,12 @@ def load_tensorboard_logs(directory: str) -> dict: The latest available scalar values. """ + # replace any non-alnum/underscore/dot with "_", then collapse runs of "_" + def replace_invalid_chars(t): + t2 = re.sub(r"[^0-9A-Za-z_./]", "_", t) + t2 = re.sub(r"_+", "_", t2) + return t2.strip("_") + # Initialize the event accumulator with a size guidance for only the latest entry def get_latest_scalars(path: str) -> dict: event_acc = EventAccumulator(path, size_guidance={"scalars": 1}) @@ -39,7 +46,7 @@ def get_latest_scalars(path: str) -> dict: event_acc.Reload() if event_acc.Tags()["scalars"]: return { - tag: event_acc.Scalars(tag)[-1].value + replace_invalid_chars(tag): event_acc.Scalars(tag)[-1].value for tag in event_acc.Tags()["scalars"] if event_acc.Scalars(tag) } @@ -64,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}") @@ -104,6 +111,12 @@ def remote_execute_job( ) +class LogExtractionError(Exception): + """Raised when we cannot extract experiment_name/logdir from the trainer output.""" + + pass + + def execute_job( job_cmd: str, identifier_string: str = "job 0", @@ -111,6 +124,8 @@ def execute_job( extract_experiment: bool = False, persistent_dir: str | None = None, log_all_output: bool = False, + max_lines_to_search_logs: int = 1000, + max_time_to_search_logs: float = 200.0, ) -> str | dict: """Issue a job (shell command). @@ -123,6 +138,8 @@ def execute_job( persistent_dir: When supplied, change to run the directory in a persistent directory. Can be used to avoid losing logs in the /tmp directory. Defaults to None. log_all_output: When true, print all output to the console. Defaults to False. + max_lines_to_search_logs: Maximum number of lines to search for experiment info. Defaults to 1000. + max_time_to_search_logs: Maximum time to wait for experiment info before giving up. Defaults to 200.0 seconds. Raises: ValueError: If the job is unable to start, or throws an error. Most likely to happen due to running out of memory. @@ -196,6 +213,8 @@ def execute_job( process = subprocess.Popen( job_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1 ) + process_file_descriptor = process.stdout.fileno() + if persistent_dir: os.chdir(og_dir) experiment_name = None @@ -211,48 +230,80 @@ def stream_reader(stream, identifier_string, result_details): if log_all_output: print(f"{identifier_string}: {line}") - # Read stdout until we find experiment info + # Read stdout until we find exp. info, up to max_lines_to_search_logs lines, max_time_to_search_logs, or EOF. # Do some careful handling prevent overflowing the pipe reading buffer with error 141 - for line in iter(process.stdout.readline, ""): - line = line.strip() - result_details.append(f"{identifier_string}: {line} \n") - if log_all_output: - print(f"{identifier_string}: {line}") - - if extract_experiment: - exp_match = experiment_info_pattern.search(line) - log_match = logdir_pattern.search(line) - err_match = err_pattern.search(line) - - if err_match: - raise ValueError(f"Encountered an error during trial run. {' '.join(result_details)}") - - if exp_match: - experiment_name = exp_match.group(1) - if log_match: - logdir = log_match.group(1) - - if experiment_name and logdir: - # Start stderr reader after finding experiment info - stderr_thread = threading.Thread( - target=stream_reader, args=(process.stderr, identifier_string, result_details) - ) - stderr_thread.daemon = True - stderr_thread.start() - - # Start stdout reader to continue reading to flush buffer - stdout_thread = threading.Thread( - target=stream_reader, args=(process.stdout, identifier_string, result_details) - ) - stdout_thread.daemon = True - stdout_thread.start() - - return { - "experiment_name": experiment_name, - "logdir": logdir, - "proc": process, - "result": " ".join(result_details), - } + lines_read = 0 + search_duration = 0.0 + search_start_time = time() + while True: + new_line_ready, _, _ = select.select([process_file_descriptor], [], [], 1.0) # Wait up to 1s for stdout + if new_line_ready: + line = process.stdout.readline() + if not line: # EOF + break + + lines_read += 1 + line = line.strip() + result_details.append(f"{identifier_string}: {line} \n") + + if log_all_output: + print(f"{identifier_string}: {line}") + + if extract_experiment: + exp_match = experiment_info_pattern.search(line) + log_match = logdir_pattern.search(line) + err_match = err_pattern.search(line) + + if err_match: + raise ValueError(f"Encountered an error during trial run. {' '.join(result_details)}") + + if exp_match: + experiment_name = exp_match.group(1) + if log_match: + logdir = log_match.group(1) + + if experiment_name and logdir: + # Start stderr reader after finding experiment info + stderr_thread = threading.Thread( + target=stream_reader, args=(process.stderr, identifier_string, result_details) + ) + stderr_thread.daemon = True + stderr_thread.start() + + # Start stdout reader to continue reading to flush buffer + stdout_thread = threading.Thread( + target=stream_reader, args=(process.stdout, identifier_string, result_details) + ) + stdout_thread.daemon = True + stdout_thread.start() + + return { + "experiment_name": experiment_name, + "logdir": logdir, + "proc": process, + "result": " ".join(result_details), + } + + if extract_experiment: # if we are looking for experiment info, check for timeouts and line limits + search_duration = time() - search_start_time + if search_duration > max_time_to_search_logs: + print(f"[ERROR]: Could not find experiment logs within {max_time_to_search_logs} seconds.") + break + if lines_read >= max_lines_to_search_logs: + print(f"[ERROR]: Could not find experiment logs within first {max_lines_to_search_logs} lines.") + break + + # If we reach here, we didn't find experiment info in the output + if extract_experiment and not (experiment_name and logdir): + error_msg = ( + "Could not extract experiment_name/logdir from trainer output " + f"(experiment_name={experiment_name!r}, logdir={logdir!r}).\n" + "\tMake sure your training script prints the following correctly:\n" + "\t\tExact experiment name requested from command line: \n" + "\t\t[INFO] Logging experiment in directory: \n\n" + ) + print(f"[ERROR]: {error_msg}") + raise LogExtractionError("Could not extract experiment_name/logdir from training workflow output.") process.wait() now = datetime.now().strftime("%H:%M:%S.%f") completion_info = f"\n[INFO]: {identifier_string}: Job Started at {start_time}, completed at {now}\n" @@ -261,12 +312,23 @@ def stream_reader(stream, identifier_string, result_details): return " ".join(result_details) +def ray_init(ray_address: str = "auto", runtime_env: dict[str, Any] | None = None, log_to_driver: bool = False): + """Initialize Ray with the given address and runtime environment.""" + if not ray.is_initialized(): + print( + f"[INFO] Initializing Ray with address {ray_address}, log_to_driver={log_to_driver}," + 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( total_resources: bool = False, one_node_only: bool = False, include_gb_ram: bool = False, include_id: bool = False, - ray_address: str = "auto", ) -> list[dict] | dict: """Get information about available GPU node resources. @@ -283,8 +345,7 @@ def get_gpu_node_resources( or simply the resource for a single node if requested. """ if not ray.is_initialized(): - ray.init(address=ray_address) - + raise RuntimeError("Ray must be initialized before calling get_gpu_node_resources().") nodes = ray.nodes() node_resources = [] total_cpus = 0 @@ -435,3 +496,225 @@ def _dicts_equal(d1: dict, d2: dict, tol=1e-9) -> bool: elif d1[key] != d2[key]: return False return True + + +@dataclass +class JobResource: + """A dataclass to represent a resource request for a job.""" + + num_gpus: float | None = None + num_cpus: float | None = None + memory: int | None = None # in bytes + + def to_opt(self) -> dict[str, Any]: + """Convert the resource request to a dictionary.""" + opt = {} + if self.num_gpus is not None: + opt["num_gpus"] = self.num_gpus + if self.num_cpus is not None: + opt["num_cpus"] = self.num_cpus + if self.memory is not None: + opt["memory"] = self.memory + return opt + + def to_pg_resources(self) -> dict[str, Any]: + """Convert the resource request to a dictionary suitable for placement groups.""" + res = {} + if self.num_gpus is not None: + res["GPU"] = self.num_gpus + if self.num_cpus is not None: + res["CPU"] = self.num_cpus + if self.memory is not None: + res["memory"] = self.memory + return res + + +@dataclass +class JobNode: + """A dataclass to represent a node for job affinity.""" + + specific: str | None = None + hostname: str | None = None + node_id: str | None = None + + def to_opt(self, nodes: list[dict[str, Any]]) -> dict[str, Any]: + """ + Convert node affinity settings into a dictionary of Ray actor scheduling options. + + Args: + nodes (list[dict[str, Any]]): List of node metadata from `ray.nodes()` which looks like this: + [{ + 'NodeID': 'xxx', + 'Alive': True, + 'NodeManagerAddress': 'x.x.x.x', + 'NodeManagerHostname': 'ray-head-mjzzf', + 'NodeManagerPort': 44039, + 'ObjectManagerPort': 35689, + 'ObjectStoreSocketName': '/tmp/ray/session_xxx/sockets/plasma_store', + 'RayletSocketName': '/tmp/ray/session_xxx/sockets/raylet', + 'MetricsExportPort': 8080, + 'NodeName': 'x.x.x.x', + 'RuntimeEnvAgentPort': 63725, + 'DeathReason': 0, + 'DeathReasonMessage': '', + 'alive': True, + 'Resources': { + 'node:__internal_head__': 1.0, + 'object_store_memory': 422449279795.0, + 'memory': 1099511627776.0, + 'GPU': 8.0, + 'node:x.x.x.x': 1.0, + 'CPU': 192.0, + 'accelerator_type:H20': 1.0 + }, + 'Labels': { + 'ray.io/node_id': 'xxx' + } + },...] + + Returns: + dict[str, Any]: A dictionary with possible scheduling options: + - Empty if no specific placement requirement. + - "scheduling_strategy" key set to `NodeAffinitySchedulingStrategy` + if hostname or node_id placement is specified. + + Raises: + ValueError: If hostname/node_id is specified but not found in the cluster + or the node is not alive. + """ + opt = {} + if self.specific is None or self.specific == "any": + return opt + elif self.specific == "hostname": + if self.hostname is None: + raise ValueError("Hostname must be specified when specific is 'hostname'") + for node in nodes: + if node["NodeManagerHostname"] == self.hostname: + if node["alive"] is False: + raise ValueError(f"Node {node['NodeID']} is not alive") + opt["scheduling_strategy"] = NodeAffinitySchedulingStrategy(node_id=node["NodeID"], soft=False) + return opt + raise ValueError(f"Hostname {self.hostname} not found in nodes: {nodes}") + elif self.specific == "node_id": + if self.node_id is None: + raise ValueError("Node ID must be specified when specific is 'node_id'") + for node in nodes: + if node["NodeID"] == self.node_id: + if node["alive"] is False: + raise ValueError(f"Node {node['NodeID']} is not alive") + opt["scheduling_strategy"] = NodeAffinitySchedulingStrategy(node_id=node["NodeID"], soft=False) + return opt + raise ValueError(f"Node ID {self.node_id} not found in nodes: {nodes}") + else: + raise ValueError(f"Invalid specific value: {self.specific}. Must be 'any', 'hostname', or 'node_id'.") + + +@dataclass +class Job: + """A dataclass to represent a job to be submitted to Ray.""" + + # job command + cmd: str | None = None + py_args: str | None = None + # identifier string for the job, e.g., "job 0" + name: str = "" + # job resources, e.g., {"CPU": 4, "GPU": 1} + resources: JobResource | None = None + # specify the node to run the job on, if needed to run on a specific node + node: JobNode | None = None + + def to_opt(self, nodes: list[dict[str, Any]]) -> dict[str, Any]: + """ + Convert the job definition into a dictionary of Ray scheduling options. + + Args: + nodes (list[dict[str, Any]]): Node information from `ray.nodes()`. + + Returns: + dict[str, Any]: Combined scheduling options from: + - `JobResource.to_opt()` for resource requirements + - `JobNode.to_opt()` for node placement constraints + """ + opt = {} + if self.resources is not None: + opt.update(self.resources.to_opt()) + if self.node is not None: + opt.update(self.node.to_opt(nodes)) + return opt + + +@ray.remote +class JobActor: + """Actor to run job in Ray cluster.""" + + def __init__(self, job: Job, test_mode: bool, log_all_output: bool, extract_experiment: bool = False): + self.job = job + self.test_mode = test_mode + self.log_all_output = log_all_output + self.extract_experiment = extract_experiment + self.done = True + + def ready(self) -> bool: + """Check if the job is ready to run.""" + return self.done + + def run(self): + """Run the job.""" + cmd = self.job.cmd if self.job.cmd else " ".join([sys.executable, *self.job.py_args.split()]) + return execute_job( + job_cmd=cmd, + identifier_string=self.job.name, + test_mode=self.test_mode, + extract_experiment=self.extract_experiment, + log_all_output=self.log_all_output, + ) + + +def submit_wrapped_jobs( + jobs: Sequence[Job], + log_realtime: bool = True, + test_mode: bool = False, + concurrent: bool = False, +) -> None: + """ + Submit a list of jobs to the Ray cluster and manage their execution. + + Args: + jobs (Sequence[Job]): A sequence of Job objects to execute on Ray. + log_realtime (bool): Whether to log stdout/stderr in real-time. Defaults to True. + test_mode (bool): If True, run in GPU sanity-check mode instead of actual jobs. Defaults to False. + concurrent (bool): Whether to launch tasks simultaneously as a batch, + or independently as resources become available. Defaults to False. + + Returns: + None + """ + if jobs is None or len(jobs) == 0: + print("[WARNING]: No jobs to submit") + return + if not ray.is_initialized(): + raise Exception("Ray is not initialized. Please initialize Ray before submitting jobs.") + nodes = ray.nodes() + actors = [] + for i, job in enumerate(jobs): + opts = job.to_opt(nodes) + name = job.name or f"job_{i + 1}" + print(f"[INFO] Create {name} with opts={opts}") + job_actor = JobActor.options(**opts).remote(job, test_mode, log_realtime) + actors.append(job_actor) + try: + if concurrent: + ray.get([actor.ready.remote() for actor in actors]) + print("[INFO] All actors are ready to run.") + future = [actor.run.remote() for actor in actors] + while future: + ready, not_ready = ray.wait(future, timeout=5) + for result in ray.get(ready): + print(f"\n{result}\n") + future = not_ready + print("[INFO] all jobs completed.") + except KeyboardInterrupt: + print("[INFO] KeyboardInterrupt received, cancelling …") + for actor in actors: + ray.cancel(actor, force=True) + sys.exit(0) diff --git a/scripts/reinforcement_learning/ray/wrap_resources.py b/scripts/reinforcement_learning/ray/wrap_resources.py index 25ec658..127b176 100644 --- a/scripts/reinforcement_learning/ray/wrap_resources.py +++ b/scripts/reinforcement_learning/ray/wrap_resources.py @@ -1,19 +1,8 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -import argparse - -import ray -import util -from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy - """ This script dispatches sub-job(s) (individual jobs, use :file:`tuner.py` for tuning jobs) to worker(s) on GPU-enabled node(s) of a specific cluster as part of an resource-wrapped aggregate @@ -69,6 +58,10 @@ ./isaaclab.sh -p scripts/reinforcement_learning/ray/wrap_resources.py -h """ +import argparse + +import util + def wrap_resources_to_jobs(jobs: list[str], args: argparse.Namespace) -> None: """ @@ -80,9 +73,14 @@ def wrap_resources_to_jobs(jobs: list[str], args: argparse.Namespace) -> None: args: The arguments for resource allocation """ - if not ray.is_initialized(): - ray.init(address=args.ray_address, log_to_driver=True) - job_results = [] + job_objs = [] + util.ray_init( + ray_address=args.ray_address, + runtime_env={ + "py_modules": None if not args.py_modules else args.py_modules, + }, + log_to_driver=False, + ) gpu_node_resources = util.get_gpu_node_resources(include_id=True, include_gb_ram=True) if any([args.gpu_per_worker, args.cpu_per_worker, args.ram_gb_per_worker]) and args.num_workers: @@ -102,7 +100,7 @@ def wrap_resources_to_jobs(jobs: list[str], args: argparse.Namespace) -> None: jobs = ["nvidia-smi"] * num_nodes for i, job in enumerate(jobs): gpu_node = gpu_node_resources[i % num_nodes] - print(f"[INFO]: Submitting job {i + 1} of {len(jobs)} with job '{job}' to node {gpu_node}") + print(f"[INFO]: Creating job {i + 1} of {len(jobs)} with job '{job}' to node {gpu_node}") print( f"[INFO]: Resource parameters: GPU: {args.gpu_per_worker[i]}" f" CPU: {args.cpu_per_worker[i]} RAM {args.ram_gb_per_worker[i]}" @@ -111,19 +109,19 @@ def wrap_resources_to_jobs(jobs: list[str], args: argparse.Namespace) -> None: num_gpus = args.gpu_per_worker[i] / args.num_workers[i] num_cpus = args.cpu_per_worker[i] / args.num_workers[i] memory = (args.ram_gb_per_worker[i] * 1024**3) / args.num_workers[i] - print(f"[INFO]: Requesting {num_gpus=} {num_cpus=} {memory=} id={gpu_node['id']}") - job = util.remote_execute_job.options( - num_gpus=num_gpus, - num_cpus=num_cpus, - memory=memory, - scheduling_strategy=NodeAffinitySchedulingStrategy(gpu_node["id"], soft=False), - ).remote(job, f"Job {i}", args.test) - job_results.append(job) - - results = ray.get(job_results) - for i, result in enumerate(results): - print(f"[INFO]: Job {i} result: {result}") - print("[INFO]: All jobs completed.") + job_objs.append( + util.Job( + cmd=job, + name=f"Job-{i + 1}", + resources=util.JobResource(num_gpus=num_gpus, num_cpus=num_cpus, memory=memory), + node=util.JobNode( + specific="node_id", + node_id=gpu_node["id"], + ), + ) + ) + # submit jobs + util.submit_wrapped_jobs(jobs=job_objs, test_mode=args.test, concurrent=False) if __name__ == "__main__": @@ -139,6 +137,15 @@ def wrap_resources_to_jobs(jobs: list[str], args: argparse.Namespace) -> None: "that GPU resources are correctly isolated." ), ) + parser.add_argument( + "--py_modules", + type=str, + nargs="*", + default=[], + help=( + "List of python modules or paths to add before running the job. Example: --py_modules my_package/my_package" + ), + ) parser.add_argument( "--sub_jobs", type=str, diff --git a/scripts/reinforcement_learning/rl_games/play.py b/scripts/reinforcement_learning/rl_games/play.py index ae5fc36..202bebe 100644 --- a/scripts/reinforcement_learning/rl_games/play.py +++ b/scripts/reinforcement_learning/rl_games/play.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -13,6 +8,7 @@ """Launch Isaac Sim Simulator first.""" import argparse +import sys from isaaclab.app import AppLauncher @@ -25,7 +21,11 @@ ) parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", type=str, default="rl_games_cfg_entry_point", help="Name of the RL agent configuration entry point." +) parser.add_argument("--checkpoint", type=str, default=None, help="Path to model checkpoint.") +parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--use_pretrained_checkpoint", action="store_true", @@ -40,11 +40,13 @@ # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments -args_cli = parser.parse_args() +args_cli, hydra_args = parser.parse_known_args() # always enable cameras to record video if args_cli.video: args_cli.enable_cameras = True +# clear out sys.argv for Hydra +sys.argv = [sys.argv[0]] + hydra_args # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -55,6 +57,7 @@ import gymnasium as gym import math import os +import random import time import torch @@ -62,7 +65,13 @@ from rl_games.common.player import BasePlayer from rl_games.torch_runner import Runner -from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent +from isaaclab.envs import ( + DirectMARLEnv, + DirectMARLEnvCfg, + DirectRLEnvCfg, + ManagerBasedRLEnvCfg, + multi_agent_to_single_agent, +) 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 @@ -71,16 +80,35 @@ import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils import get_checkpoint_path, load_cfg_from_registry, parse_env_cfg +from isaaclab_tasks.utils import get_checkpoint_path +from uwlab_tasks.utils.hydra import hydra_task_config + +# PLACEHOLDER: Extension template (do not remove this comment) -def main(): +@hydra_task_config(args_cli.task, args_cli.agent) +def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: dict): """Play with RL-Games agent.""" - # parse env configuration - env_cfg = parse_env_cfg( - args_cli.task, device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric - ) - agent_cfg = load_cfg_from_registry(args_cli.task, "rl_games_cfg_entry_point") + # grab task name for checkpoint path + task_name = args_cli.task.split(":")[-1] + train_task_name = task_name.replace("-Play", "") + + # 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: + args_cli.seed = random.randint(0, 10000) + + agent_cfg["params"]["seed"] = args_cli.seed if args_cli.seed is not None else agent_cfg["params"]["seed"] + # set the environment seed (after multi-gpu config for updated rank from agent seed) + # note: certain randomizations occur in the environment initialization so we set the seed here + env_cfg.seed = agent_cfg["params"]["seed"] # specify directory for logging experiments log_root_path = os.path.join("logs", "rl_games", agent_cfg["params"]["config"]["name"]) @@ -88,7 +116,7 @@ def main(): print(f"[INFO] Loading experiment from directory: {log_root_path}") # find checkpoint if args_cli.use_pretrained_checkpoint: - resume_path = get_published_pretrained_checkpoint("rl_games", args_cli.task) + resume_path = get_published_pretrained_checkpoint("rl_games", train_task_name) if not resume_path: print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.") return @@ -107,10 +135,15 @@ def main(): resume_path = retrieve_file_path(args_cli.checkpoint) log_dir = os.path.dirname(os.path.dirname(resume_path)) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # wrap around environment for rl-games rl_device = agent_cfg["params"]["config"]["device"] clip_obs = agent_cfg["params"]["env"].get("clip_observations", math.inf) clip_actions = agent_cfg["params"]["env"].get("clip_actions", math.inf) + obs_groups = agent_cfg["params"]["env"].get("obs_groups") + concate_obs_groups = agent_cfg["params"]["env"].get("concate_obs_groups", True) # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) @@ -132,7 +165,7 @@ def main(): env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for rl-games - env = RlGamesVecEnvWrapper(env, rl_device, clip_obs, clip_actions) + env = RlGamesVecEnvWrapper(env, rl_device, clip_obs, clip_actions, obs_groups, concate_obs_groups) # register the environment to rl-games registry # note: in agents configuration: environment name must be "rlgpu" @@ -156,7 +189,7 @@ def main(): agent.restore(resume_path) agent.reset() - dt = env.unwrapped.physics_dt + dt = env.unwrapped.step_dt # reset environment obs = env.reset() @@ -191,7 +224,7 @@ def main(): s[:, dones, :] = 0.0 if args_cli.video: timestep += 1 - # Exit the play loop after recording one video + # exit the play loop after recording one video if timestep == args_cli.video_length: break diff --git a/scripts/reinforcement_learning/rl_games/train.py b/scripts/reinforcement_learning/rl_games/train.py index 4c64fac..8a680bf 100644 --- a/scripts/reinforcement_learning/rl_games/train.py +++ b/scripts/reinforcement_learning/rl_games/train.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -14,6 +9,7 @@ import argparse import sys +from distutils.util import strtobool from isaaclab.app import AppLauncher @@ -24,6 +20,9 @@ parser.add_argument("--video_interval", type=int, default=2000, help="Interval between video recordings (in steps).") parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", type=str, default="rl_games_cfg_entry_point", help="Name of the RL agent configuration entry point." +) parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--distributed", action="store_true", default=False, help="Run training with multiple GPUs or nodes." @@ -31,7 +30,21 @@ parser.add_argument("--checkpoint", type=str, default=None, help="Path to model checkpoint.") parser.add_argument("--sigma", type=str, default=None, help="The policy's initial standard deviation.") parser.add_argument("--max_iterations", type=int, default=None, help="RL Policy training iterations.") - +parser.add_argument("--wandb-project-name", type=str, default=None, help="the wandb's project name") +parser.add_argument("--wandb-entity", type=str, default=None, help="the entity (team) of wandb's project") +parser.add_argument("--wandb-name", type=str, default=None, help="the name of wandb's run") +parser.add_argument( + "--track", + type=lambda x: bool(strtobool(x)), + default=False, + nargs="?", + const=True, + 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 @@ -50,6 +63,7 @@ """Rest everything follows.""" import gymnasium as gym +import logging import math import os import random @@ -68,21 +82,37 @@ ) from isaaclab.utils.assets import retrieve_file_path from isaaclab.utils.dict import print_dict -from isaaclab.utils.io import dump_pickle, dump_yaml +from isaaclab.utils.io import dump_yaml -from isaaclab_rl.rl_games import RlGamesGpuEnv, RlGamesVecEnvWrapper +from isaaclab_rl.rl_games import MultiObserver, PbtAlgoObserver, RlGamesGpuEnv, RlGamesVecEnvWrapper import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils.hydra import hydra_task_config +from uwlab_tasks.utils.hydra import hydra_task_config + +# import logger +logger = logging.getLogger(__name__) +# PLACEHOLDER: Extension template (do not remove this comment) -@hydra_task_config(args_cli.task, "rl_games_cfg_entry_point") + +@hydra_task_config(args_cli.task, args_cli.agent) def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: dict): """Train with RL-Games agent.""" # 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 + # check for invalid combination of CPU device with distributed training + if args_cli.distributed and args_cli.device is not None and "cpu" in args_cli.device: + raise ValueError( + "Distributed training is not supported when using CPU device. " + "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: @@ -113,8 +143,13 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen env_cfg.seed = agent_cfg["params"]["seed"] # specify directory for logging experiments - log_root_path = os.path.join("logs", "rl_games", agent_cfg["params"]["config"]["name"]) - log_root_path = os.path.abspath(log_root_path) + config_name = agent_cfg["params"]["config"]["name"] + log_root_path = os.path.join("logs", "rl_games", config_name) + 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 log_dir = agent_cfg["params"]["config"].get("full_experiment_name", datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) @@ -122,17 +157,31 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # logging directory path: / agent_cfg["params"]["config"]["train_dir"] = log_root_path agent_cfg["params"]["config"]["full_experiment_name"] = log_dir + wandb_project = config_name if args_cli.wandb_project_name is None else args_cli.wandb_project_name + experiment_name = log_dir if args_cli.wandb_name is None else args_cli.wandb_name # 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) - dump_pickle(os.path.join(log_root_path, log_dir, "params", "env.pkl"), env_cfg) - dump_pickle(os.path.join(log_root_path, log_dir, "params", "agent.pkl"), 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"] clip_obs = agent_cfg["params"]["env"].get("clip_observations", math.inf) clip_actions = agent_cfg["params"]["env"].get("clip_actions", math.inf) + obs_groups = agent_cfg["params"]["env"].get("obs_groups") + concate_obs_groups = agent_cfg["params"]["env"].get("concate_obs_groups", True) + + # set the IO descriptors export flag if requested + if isinstance(env_cfg, ManagerBasedRLEnvCfg): + env_cfg.export_io_descriptors = args_cli.export_io_descriptors + else: + logger.warning( + "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." + ) + + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = os.path.join(log_root_path, log_dir) # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) @@ -154,7 +203,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for rl-games - env = RlGamesVecEnvWrapper(env, rl_device, clip_obs, clip_actions) + env = RlGamesVecEnvWrapper(env, rl_device, clip_obs, clip_actions, obs_groups, concate_obs_groups) # register the environment to rl-games registry # note: in agents configuration: environment name must be "rlgpu" @@ -166,12 +215,37 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # set number of actors into agent config agent_cfg["params"]["config"]["num_actors"] = env.unwrapped.num_envs # create runner from rl-games - runner = Runner(IsaacAlgoObserver()) + + if "pbt" in agent_cfg and agent_cfg["pbt"]["enabled"]: + observers = MultiObserver([IsaacAlgoObserver(), PbtAlgoObserver(agent_cfg, args_cli)]) + runner = Runner(observers) + else: + runner = Runner(IsaacAlgoObserver()) + runner.load(agent_cfg) # reset the agent and env runner.reset() # train the agent + + global_rank = int(os.getenv("RANK", "0")) + if args_cli.track and global_rank == 0: + if args_cli.wandb_entity is None: + raise ValueError("Weights and Biases entity must be specified for tracking.") + import wandb + + wandb.init( + project=wandb_project, + entity=args_cli.wandb_entity, + name=experiment_name, + sync_tensorboard=True, + monitor_gym=True, + save_code=True, + ) + if not wandb.run.resumed: + wandb.config.update({"env_cfg": env_cfg.to_dict()}) + wandb.config.update({"agent_cfg": agent_cfg}) + if args_cli.checkpoint is not None: runner.run({"train": True, "play": False, "sigma": train_sigma, "checkpoint": resume_path}) else: diff --git a/scripts/reinforcement_learning/rsl_rl/cli_args.py b/scripts/reinforcement_learning/rsl_rl/cli_args.py index b1485a2..2e3a546 100644 --- a/scripts/reinforcement_learning/rsl_rl/cli_args.py +++ b/scripts/reinforcement_learning/rsl_rl/cli_args.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -15,7 +10,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg + from isaaclab_rl.rsl_rl import RslRlBaseRunnerCfg def add_rsl_rl_args(parser: argparse.ArgumentParser): @@ -32,7 +27,7 @@ def add_rsl_rl_args(parser: argparse.ArgumentParser): ) arg_group.add_argument("--run_name", type=str, default=None, help="Run name suffix to the log directory.") # -- load arguments - arg_group.add_argument("--resume", type=bool, default=None, help="Whether to resume from a checkpoint.") + arg_group.add_argument("--resume", action="store_true", default=False, help="Whether to resume from a checkpoint.") arg_group.add_argument("--load_run", type=str, default=None, help="Name of the run folder to resume from.") arg_group.add_argument("--checkpoint", type=str, default=None, help="Checkpoint file to resume from.") # -- logger arguments @@ -44,7 +39,7 @@ def add_rsl_rl_args(parser: argparse.ArgumentParser): ) -def parse_rsl_rl_cfg(task_name: str, args_cli: argparse.Namespace) -> RslRlOnPolicyRunnerCfg: +def parse_rsl_rl_cfg(task_name: str, args_cli: argparse.Namespace) -> RslRlBaseRunnerCfg: """Parse configuration for RSL-RL agent based on inputs. Args: @@ -57,12 +52,12 @@ def parse_rsl_rl_cfg(task_name: str, args_cli: argparse.Namespace) -> RslRlOnPol from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry # load the default configuration - rslrl_cfg: RslRlOnPolicyRunnerCfg = load_cfg_from_registry(task_name, "rsl_rl_cfg_entry_point") + rslrl_cfg: RslRlBaseRunnerCfg = load_cfg_from_registry(task_name, "rsl_rl_cfg_entry_point") rslrl_cfg = update_rsl_rl_cfg(rslrl_cfg, args_cli) return rslrl_cfg -def update_rsl_rl_cfg(agent_cfg: RslRlOnPolicyRunnerCfg, args_cli: argparse.Namespace): +def update_rsl_rl_cfg(agent_cfg: RslRlBaseRunnerCfg, args_cli: argparse.Namespace): """Update configuration for RSL-RL agent based on inputs. Args: diff --git a/scripts/reinforcement_learning/rsl_rl/play.py b/scripts/reinforcement_learning/rsl_rl/play.py index b1ae6ad..483a86c 100644 --- a/scripts/reinforcement_learning/rsl_rl/play.py +++ b/scripts/reinforcement_learning/rsl_rl/play.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -13,6 +8,7 @@ """Launch Isaac Sim Simulator first.""" import argparse +import sys from isaaclab.app import AppLauncher @@ -28,6 +24,10 @@ ) parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", type=str, default="rsl_rl_cfg_entry_point", help="Name of the RL agent configuration entry point." +) +parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--use_pretrained_checkpoint", action="store_true", @@ -38,11 +38,15 @@ cli_args.add_rsl_rl_args(parser) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -args_cli = parser.parse_args() +# parse the arguments +args_cli, hydra_args = parser.parse_known_args() # always enable cameras to record video if args_cli.video: args_cli.enable_cameras = True +# clear out sys.argv for Hydra +sys.argv = [sys.argv[0]] + hydra_args + # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -54,34 +58,51 @@ import time import torch -from rsl_rl.runners import OnPolicyRunner +from rsl_rl.runners import DistillationRunner, OnPolicyRunner -from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent +from isaaclab.envs import ( + DirectMARLEnv, + DirectMARLEnvCfg, + DirectRLEnvCfg, + ManagerBasedRLEnvCfg, + multi_agent_to_single_agent, +) 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 RslRlOnPolicyRunnerCfg, RslRlVecEnvWrapper, export_policy_as_jit, export_policy_as_onnx +from isaaclab_rl.rsl_rl import RslRlBaseRunnerCfg, RslRlVecEnvWrapper, export_policy_as_jit, export_policy_as_onnx import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils import get_checkpoint_path, parse_env_cfg +from isaaclab_tasks.utils import get_checkpoint_path +from uwlab_tasks.utils.hydra import hydra_task_config +# PLACEHOLDER: Extension template (do not remove this comment) -def main(): + +@hydra_task_config(args_cli.task, args_cli.agent) +def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlBaseRunnerCfg): """Play with RSL-RL agent.""" - # parse configuration - env_cfg = parse_env_cfg( - args_cli.task, device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric - ) - agent_cfg: RslRlOnPolicyRunnerCfg = cli_args.parse_rsl_rl_cfg(args_cli.task, args_cli) + # grab task name for checkpoint path + task_name = args_cli.task.split(":")[-1] + train_task_name = task_name.replace("-Play", "") + + # override configurations with non-hydra CLI arguments + agent_cfg: RslRlBaseRunnerCfg = cli_args.update_rsl_rl_cfg(agent_cfg, args_cli) + env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs + + # set the environment seed + # note: certain randomizations occur in the environment initialization so we set the seed here + env_cfg.seed = agent_cfg.seed + env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device # specify directory for logging experiments log_root_path = os.path.join("logs", "rsl_rl", agent_cfg.experiment_name) log_root_path = os.path.abspath(log_root_path) print(f"[INFO] Loading experiment from directory: {log_root_path}") if args_cli.use_pretrained_checkpoint: - resume_path = get_published_pretrained_checkpoint("rsl_rl", args_cli.task) + resume_path = get_published_pretrained_checkpoint("rsl_rl", train_task_name) if not resume_path: print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.") return @@ -92,6 +113,9 @@ def main(): log_dir = os.path.dirname(resume_path) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) @@ -112,29 +136,47 @@ def main(): env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for rsl-rl - env = RslRlVecEnvWrapper(env) + env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions) print(f"[INFO]: Loading model checkpoint from: {resume_path}") # load previously trained model - ppo_runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device) - ppo_runner.load(resume_path) + if agent_cfg.class_name == "OnPolicyRunner": + runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device) + elif agent_cfg.class_name == "DistillationRunner": + runner = DistillationRunner(env, agent_cfg.to_dict(), log_dir=None, device=agent_cfg.device) + else: + raise ValueError(f"Unsupported runner class: {agent_cfg.class_name}") + runner.load(resume_path) # obtain the trained policy for inference - policy = ppo_runner.get_inference_policy(device=env.unwrapped.device) + policy = runner.get_inference_policy(device=env.unwrapped.device) + + # extract the neural network module + # we do this in a try-except to maintain backwards compatibility. + try: + # version 2.3 onwards + policy_nn = runner.alg.policy + except AttributeError: + # version 2.2 and below + policy_nn = runner.alg.actor_critic + + # extract the normalizer + if hasattr(policy_nn, "actor_obs_normalizer"): + normalizer = policy_nn.actor_obs_normalizer + elif hasattr(policy_nn, "student_obs_normalizer"): + normalizer = policy_nn.student_obs_normalizer + else: + normalizer = None # export policy to onnx/jit export_model_dir = os.path.join(os.path.dirname(resume_path), "exported") - export_policy_as_jit( - ppo_runner.alg.actor_critic, ppo_runner.obs_normalizer, path=export_model_dir, filename="policy.pt" - ) - export_policy_as_onnx( - ppo_runner.alg.actor_critic, normalizer=ppo_runner.obs_normalizer, path=export_model_dir, filename="policy.onnx" - ) + export_policy_as_jit(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.pt") + export_policy_as_onnx(policy_nn, normalizer=normalizer, path=export_model_dir, filename="policy.onnx") - dt = env.unwrapped.physics_dt + dt = env.unwrapped.step_dt # reset environment - obs, _ = env.get_observations() + obs = env.get_observations() timestep = 0 # simulate environment while simulation_app.is_running(): @@ -144,7 +186,9 @@ def main(): # 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 20f90ad..f0c2eb0 100644 --- a/scripts/reinforcement_learning/rsl_rl/train.py +++ b/scripts/reinforcement_learning/rsl_rl/train.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -20,7 +15,6 @@ # local imports import cli_args # isort: skip - # add argparse arguments parser = argparse.ArgumentParser(description="Train an RL agent with RSL-RL.") parser.add_argument("--video", action="store_true", default=False, help="Record videos during training.") @@ -28,8 +22,18 @@ parser.add_argument("--video_interval", type=int, default=2000, help="Interval between video recordings (in steps).") parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", type=str, default="rsl_rl_cfg_entry_point", help="Name of the RL agent configuration entry point." +) parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument("--max_iterations", type=int, default=None, help="RL Policy training iterations.") +parser.add_argument( + "--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 @@ -47,14 +51,37 @@ app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app +"""Check for minimum supported RSL-RL version.""" + +import importlib.metadata as metadata +import platform + +from packaging import version + +# check minimum supported rsl-rl version +RSL_RL_VERSION = "3.0.1" +installed_version = metadata.version("rsl-rl-lib") +if version.parse(installed_version) < version.parse(RSL_RL_VERSION): + if platform.system() == "Windows": + cmd = [r".\isaaclab.bat", "-p", "-m", "pip", "install", f"rsl-rl-lib=={RSL_RL_VERSION}"] + else: + cmd = ["./isaaclab.sh", "-p", "-m", "pip", "install", f"rsl-rl-lib=={RSL_RL_VERSION}"] + print( + f"Please install the correct version of RSL-RL.\nExisting version is: '{installed_version}'" + f" and required version is: '{RSL_RL_VERSION}'.\nTo install the correct version, run:" + f"\n\n\t{' '.join(cmd)}\n" + ) + exit(1) + """Rest everything follows.""" import gymnasium as gym +import logging import os import torch from datetime import datetime -from rsl_rl.runners import OnPolicyRunner +from rsl_rl.runners import DistillationRunner, OnPolicyRunner from isaaclab.envs import ( DirectMARLEnv, @@ -64,14 +91,19 @@ multi_agent_to_single_agent, ) from isaaclab.utils.dict import print_dict -from isaaclab.utils.io import dump_pickle, dump_yaml +from isaaclab.utils.io import dump_yaml -from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlVecEnvWrapper +from isaaclab_rl.rsl_rl import RslRlBaseRunnerCfg, RslRlVecEnvWrapper import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 from isaaclab_tasks.utils import get_checkpoint_path -from isaaclab_tasks.utils.hydra import hydra_task_config +from uwlab_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 torch.backends.cudnn.allow_tf32 = True @@ -79,51 +111,8 @@ torch.backends.cudnn.benchmark = False -def process_agent_cfg(env_cfg, agent_cfg): - if hasattr(agent_cfg.algorithm, "symmetry_cfg") and agent_cfg.algorithm.symmetry_cfg is None: - del agent_cfg.algorithm.symmetry_cfg - - if hasattr(agent_cfg.algorithm, "behavior_cloning_cfg"): - if agent_cfg.algorithm.behavior_cloning_cfg is None: - del agent_cfg.algorithm.behavior_cloning_cfg - else: - bc_cfg = agent_cfg.algorithm.behavior_cloning_cfg - if bc_cfg.experts_observation_group_cfg is not None: - import importlib - - # resolve path to the module location - mod_name, attr_name = bc_cfg.experts_observation_group_cfg.split(":") - mod = importlib.import_module(mod_name) - cfg_cls = mod - for attr in attr_name.split("."): - cfg_cls = getattr(cfg_cls, attr) - cfg = cfg_cls() - setattr(env_cfg.observations, "expert_obs", cfg) - - if hasattr(agent_cfg.algorithm, "offline_algorithm_cfg"): - if agent_cfg.algorithm.offline_algorithm_cfg is None: - del agent_cfg.algorithm.offline_algorithm_cfg - else: - if agent_cfg.algorithm.offline_algorithm_cfg.behavior_cloning_cfg is None: - del agent_cfg.algorithm.offline_algorithm_cfg.behavior_cloning_cfg - else: - bc_cfg = agent_cfg.algorithm.offline_algorithm_cfg.behavior_cloning_cfg - if bc_cfg.experts_observation_group_cfg is not None: - import importlib - - # resolve path to the module location - mod_name, attr_name = bc_cfg.experts_observation_group_cfg.split(":") - mod = importlib.import_module(mod_name) - cfg_cls = mod - for attr in attr_name.split("."): - cfg_cls = getattr(cfg_cls, attr) - cfg = cfg_cls() - setattr(env_cfg.observations, "expert_obs", cfg) - return agent_cfg - - -@hydra_task_config(args_cli.task, "rsl_rl_cfg_entry_point") -def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlOnPolicyRunnerCfg): +@hydra_task_config(args_cli.task, args_cli.agent) +def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: RslRlBaseRunnerCfg): """Train with RSL-RL agent.""" # override configurations with non-hydra CLI arguments agent_cfg = cli_args.update_rsl_rl_cfg(agent_cfg, args_cli) @@ -136,19 +125,46 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # note: certain randomizations occur in the environment initialization so we set the seed here env_cfg.seed = agent_cfg.seed env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device - agent_cfg = process_agent_cfg(env_cfg, agent_cfg) + # check for invalid combination of CPU device with distributed training + if args_cli.distributed and args_cli.device is not None and "cpu" in args_cli.device: + raise ValueError( + "Distributed training is not supported when using CPU device. " + "Please use GPU device (e.g., --device cuda) for distributed training." + ) + + # multi-gpu training configuration + if args_cli.distributed: + env_cfg.sim.device = f"cuda:{app_launcher.local_rank}" + agent_cfg.device = f"cuda:{app_launcher.local_rank}" + + # set seed to have diversity in different threads + seed = agent_cfg.seed + app_launcher.local_rank + env_cfg.seed = seed + agent_cfg.seed = seed + # specify directory for logging experiments log_root_path = os.path.join("logs", "rsl_rl", agent_cfg.experiment_name) log_root_path = os.path.abspath(log_root_path) 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") - # This way, the Ray Tune workflow can extract experiment name. + # 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}" log_dir = os.path.join(log_root_path, log_dir) + # set the IO descriptors export flag if requested + if isinstance(env_cfg, ManagerBasedRLEnvCfg): + env_cfg.export_io_descriptors = args_cli.export_io_descriptors + else: + logger.warning( + "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." + ) + + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) @@ -157,7 +173,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen env = multi_agent_to_single_agent(env) # save resume path before creating a new log_dir - if agent_cfg.resume: + if agent_cfg.resume or agent_cfg.algorithm.class_name == "Distillation": resume_path = get_checkpoint_path(log_root_path, agent_cfg.load_run, agent_cfg.load_checkpoint) # wrap for video recording @@ -173,14 +189,19 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for rsl-rl - env = RslRlVecEnvWrapper(env) + env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions) # create runner from rsl-rl - runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=log_dir, device=agent_cfg.device) + if agent_cfg.class_name == "OnPolicyRunner": + runner = OnPolicyRunner(env, agent_cfg.to_dict(), log_dir=log_dir, device=agent_cfg.device) + elif agent_cfg.class_name == "DistillationRunner": + runner = DistillationRunner(env, agent_cfg.to_dict(), log_dir=log_dir, device=agent_cfg.device) + else: + raise ValueError(f"Unsupported runner class: {agent_cfg.class_name}") # write git state to logs runner.add_git_repo_to_log(__file__) # load the checkpoint - if agent_cfg.resume: + if agent_cfg.resume or agent_cfg.algorithm.class_name == "Distillation": print(f"[INFO]: Loading model checkpoint from: {resume_path}") # load previously trained model runner.load(resume_path) @@ -188,8 +209,6 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # dump the configuration into log-directory dump_yaml(os.path.join(log_dir, "params", "env.yaml"), env_cfg) dump_yaml(os.path.join(log_dir, "params", "agent.yaml"), agent_cfg) - dump_pickle(os.path.join(log_dir, "params", "env.pkl"), env_cfg) - dump_pickle(os.path.join(log_dir, "params", "agent.pkl"), agent_cfg) # run training runner.learn(num_learning_iterations=agent_cfg.max_iterations, init_at_random_ep_len=True) diff --git a/scripts/reinforcement_learning/sb3/play.py b/scripts/reinforcement_learning/sb3/play.py index acfa215..ea51f0b 100644 --- a/scripts/reinforcement_learning/sb3/play.py +++ b/scripts/reinforcement_learning/sb3/play.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -13,6 +8,8 @@ """Launch Isaac Sim Simulator first.""" import argparse +import sys +from pathlib import Path from isaaclab.app import AppLauncher @@ -25,7 +22,11 @@ ) parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", type=str, default="sb3_cfg_entry_point", help="Name of the RL agent configuration entry point." +) parser.add_argument("--checkpoint", type=str, default=None, help="Path to model checkpoint.") +parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--use_pretrained_checkpoint", action="store_true", @@ -37,14 +38,23 @@ help="When no checkpoint provided, use the last saved model. Otherwise use the best saved model.", ) parser.add_argument("--real-time", action="store_true", default=False, help="Run in real-time, if possible.") +parser.add_argument( + "--keep_all_info", + action="store_true", + default=False, + help="Use a slower SB3 wrapper but keep all the extra training info.", +) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments -args_cli = parser.parse_args() +args_cli, hydra_args = parser.parse_known_args() + # always enable cameras to record video if args_cli.video: args_cli.enable_cameras = True +# clear out sys.argv for Hydra +sys.argv = [sys.argv[0]] + hydra_args # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -52,15 +62,21 @@ """Rest everything follows.""" import gymnasium as gym -import numpy as np import os +import random import time import torch from stable_baselines3 import PPO from stable_baselines3.common.vec_env import VecNormalize -from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent +from isaaclab.envs import ( + DirectMARLEnv, + DirectMARLEnvCfg, + DirectRLEnvCfg, + ManagerBasedRLEnvCfg, + multi_agent_to_single_agent, +) from isaaclab.utils.dict import print_dict from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint @@ -68,42 +84,59 @@ import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils.parse_cfg import get_checkpoint_path, load_cfg_from_registry, parse_env_cfg +from uwlab_tasks.utils.hydra import hydra_task_config +from isaaclab_tasks.utils.parse_cfg import get_checkpoint_path +# PLACEHOLDER: Extension template (do not remove this comment) -def main(): + +@hydra_task_config(args_cli.task, args_cli.agent) +def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: dict): """Play with stable-baselines agent.""" - # parse configuration - env_cfg = parse_env_cfg( - args_cli.task, device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric - ) - agent_cfg = load_cfg_from_registry(args_cli.task, "sb3_cfg_entry_point") + # grab task name for checkpoint path + task_name = args_cli.task.split(":")[-1] + train_task_name = task_name.replace("-Play", "") + # randomly sample a seed if seed = -1 + if args_cli.seed == -1: + args_cli.seed = random.randint(0, 10000) + + # 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 + agent_cfg["seed"] = args_cli.seed if args_cli.seed is not None else agent_cfg["seed"] + # set the environment seed + # note: certain randomizations occur in the environment initialization so we set the seed here + env_cfg.seed = agent_cfg["seed"] + env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device # directory for logging into - log_root_path = os.path.join("logs", "sb3", args_cli.task) + log_root_path = os.path.join("logs", "sb3", train_task_name) log_root_path = os.path.abspath(log_root_path) # checkpoint and log_dir stuff if args_cli.use_pretrained_checkpoint: - checkpoint_path = get_published_pretrained_checkpoint("sb3", args_cli.task) + checkpoint_path = get_published_pretrained_checkpoint("sb3", train_task_name) if not checkpoint_path: print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.") return elif args_cli.checkpoint is None: + # FIXME: last checkpoint doesn't seem to really use the last one' if args_cli.use_last_checkpoint: checkpoint = "model_.*.zip" else: checkpoint = "model.zip" - checkpoint_path = get_checkpoint_path(log_root_path, ".*", checkpoint) + checkpoint_path = get_checkpoint_path(log_root_path, ".*", checkpoint, sort_alpha=False) else: checkpoint_path = args_cli.checkpoint log_dir = os.path.dirname(checkpoint_path) - # post-process agent configuration - agent_cfg = process_sb3_cfg(agent_cfg) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) + # post-process agent configuration + agent_cfg = process_sb3_cfg(agent_cfg, env.unwrapped.num_envs) + # convert to single-agent instance if required by the RL algorithm if isinstance(env.unwrapped, DirectMARLEnv): env = multi_agent_to_single_agent(env) @@ -120,25 +153,32 @@ def main(): print_dict(video_kwargs, nesting=4) env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for stable baselines - env = Sb3VecEnvWrapper(env) + env = Sb3VecEnvWrapper(env, fast_variant=not args_cli.keep_all_info) + + vec_norm_path = checkpoint_path.replace("/model", "/model_vecnormalize").replace(".zip", ".pkl") + vec_norm_path = Path(vec_norm_path) # normalize environment (if needed) - if "normalize_input" in agent_cfg: + if vec_norm_path.exists(): + print(f"Loading saved normalization: {vec_norm_path}") + env = VecNormalize.load(vec_norm_path, env) + # do not update them at test time + env.training = False + # reward normalization is not needed at test time + env.norm_reward = False + elif "normalize_input" in agent_cfg: env = VecNormalize( env, training=True, norm_obs="normalize_input" in agent_cfg and agent_cfg.pop("normalize_input"), - norm_reward="normalize_value" in agent_cfg and agent_cfg.pop("normalize_value"), clip_obs="clip_obs" in agent_cfg and agent_cfg.pop("clip_obs"), - gamma=agent_cfg["gamma"], - clip_reward=np.inf, ) # create agent from stable baselines print(f"Loading checkpoint from: {checkpoint_path}") agent = PPO.load(checkpoint_path, env, print_system_info=True) - dt = env.unwrapped.physics_dt + dt = env.unwrapped.step_dt # reset environment obs = env.reset() diff --git a/scripts/reinforcement_learning/sb3/train.py b/scripts/reinforcement_learning/sb3/train.py index 9d32004..503a8ca 100644 --- a/scripts/reinforcement_learning/sb3/train.py +++ b/scripts/reinforcement_learning/sb3/train.py @@ -1,24 +1,18 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -"""Script to train RL agent with Stable Baselines3. -Since Stable-Baselines3 does not support buffers living on GPU directly, -we recommend using smaller number of environments. Otherwise, -there will be significant overhead in GPU->CPU transfer. -""" +"""Script to train RL agent with Stable Baselines3.""" """Launch Isaac Sim Simulator first.""" import argparse +import contextlib +import signal import sys +from pathlib import Path from isaaclab.app import AppLauncher @@ -29,8 +23,23 @@ parser.add_argument("--video_interval", type=int, default=2000, help="Interval between video recordings (in steps).") parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", type=str, default="sb3_cfg_entry_point", help="Name of the RL agent configuration entry point." +) parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") +parser.add_argument("--log_interval", type=int, default=100_000, help="Log data every n timesteps.") +parser.add_argument("--checkpoint", type=str, default=None, help="Continue the training from checkpoint.") parser.add_argument("--max_iterations", type=int, default=None, help="RL Policy training iterations.") +parser.add_argument("--export_io_descriptors", action="store_true", default=False, help="Export IO descriptors.") +parser.add_argument( + "--keep_all_info", + action="store_true", + 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 @@ -46,17 +55,35 @@ app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app + +def cleanup_pbar(*args): + """ + A small helper to stop training and + cleanup progress bar properly on ctrl+c + """ + import gc + + tqdm_objects = [obj for obj in gc.get_objects() if "tqdm" in type(obj).__name__] + for tqdm_object in tqdm_objects: + if "tqdm_rich" in type(tqdm_object).__name__: + tqdm_object.close() + raise KeyboardInterrupt + + +# disable KeyboardInterrupt override +signal.signal(signal.SIGINT, cleanup_pbar) + """Rest everything follows.""" import gymnasium as gym +import logging import numpy as np import os import random from datetime import datetime from stable_baselines3 import PPO -from stable_baselines3.common.callbacks import CheckpointCallback -from stable_baselines3.common.logger import configure +from stable_baselines3.common.callbacks import CheckpointCallback, LogEveryNTimesteps from stable_baselines3.common.vec_env import VecNormalize from isaaclab.envs import ( @@ -67,16 +94,20 @@ multi_agent_to_single_agent, ) from isaaclab.utils.dict import print_dict -from isaaclab.utils.io import dump_pickle, dump_yaml +from isaaclab.utils.io import dump_yaml from isaaclab_rl.sb3 import Sb3VecEnvWrapper, process_sb3_cfg import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils.hydra import hydra_task_config +from uwlab_tasks.utils.hydra import hydra_task_config + +# import logger +logger = logging.getLogger(__name__) +# PLACEHOLDER: Extension template (do not remove this comment) -@hydra_task_config(args_cli.task, "sb3_cfg_entry_point") +@hydra_task_config(args_cli.task, args_cli.agent) def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: dict): """Train with stable-baselines agent.""" # randomly sample a seed if seed = -1 @@ -99,20 +130,34 @@ 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) 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 dump_yaml(os.path.join(log_dir, "params", "env.yaml"), env_cfg) dump_yaml(os.path.join(log_dir, "params", "agent.yaml"), agent_cfg) - dump_pickle(os.path.join(log_dir, "params", "env.pkl"), env_cfg) - dump_pickle(os.path.join(log_dir, "params", "agent.pkl"), agent_cfg) + + # save command used to run the script + command = " ".join(sys.orig_argv) + (Path(log_dir) / "command.txt").write_text(command) # post-process agent configuration - agent_cfg = process_sb3_cfg(agent_cfg) + agent_cfg = process_sb3_cfg(agent_cfg, env_cfg.scene.num_envs) # read configurations about the agent-training policy_arch = agent_cfg.pop("policy") n_timesteps = agent_cfg.pop("n_timesteps") + # set the IO descriptors export flag if requested + if isinstance(env_cfg, ManagerBasedRLEnvCfg): + env_cfg.export_io_descriptors = args_cli.export_io_descriptors + else: + logger.warning( + "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." + ) + + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) @@ -133,31 +178,51 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for stable baselines - env = Sb3VecEnvWrapper(env) + env = Sb3VecEnvWrapper(env, fast_variant=not args_cli.keep_all_info) + + norm_keys = {"normalize_input", "normalize_value", "clip_obs"} + norm_args = {} + for key in norm_keys: + if key in agent_cfg: + norm_args[key] = agent_cfg.pop(key) - if "normalize_input" in agent_cfg: + if norm_args and norm_args.get("normalize_input"): + print(f"Normalizing input, {norm_args=}") env = VecNormalize( env, training=True, - norm_obs="normalize_input" in agent_cfg and agent_cfg.pop("normalize_input"), - norm_reward="normalize_value" in agent_cfg and agent_cfg.pop("normalize_value"), - clip_obs="clip_obs" in agent_cfg and agent_cfg.pop("clip_obs"), + norm_obs=norm_args["normalize_input"], + norm_reward=norm_args.get("normalize_value", False), + clip_obs=norm_args.get("clip_obs", 100.0), gamma=agent_cfg["gamma"], clip_reward=np.inf, ) # create agent from stable baselines - agent = PPO(policy_arch, env, verbose=1, **agent_cfg) - # configure the logger - new_logger = configure(log_dir, ["stdout", "tensorboard"]) - agent.set_logger(new_logger) + agent = PPO(policy_arch, env, verbose=1, tensorboard_log=log_dir, **agent_cfg) + if args_cli.checkpoint is not None: + agent = agent.load(args_cli.checkpoint, env, print_system_info=True) # callbacks for agent checkpoint_callback = CheckpointCallback(save_freq=1000, save_path=log_dir, name_prefix="model", verbose=2) + callbacks = [checkpoint_callback, LogEveryNTimesteps(n_steps=args_cli.log_interval)] + # train the agent - agent.learn(total_timesteps=n_timesteps, callback=checkpoint_callback) + with contextlib.suppress(KeyboardInterrupt): + agent.learn( + total_timesteps=n_timesteps, + callback=callbacks, + progress_bar=True, + log_interval=None, + ) # save the final model agent.save(os.path.join(log_dir, "model")) + print("Saving to:") + print(os.path.join(log_dir, "model.zip")) + + if isinstance(env, VecNormalize): + print("Saving normalization") + env.save(os.path.join(log_dir, "model_vecnormalize.pkl")) # close the simulator env.close() diff --git a/scripts/reinforcement_learning/skrl/play.py b/scripts/reinforcement_learning/skrl/play.py index a4c0f39..ae68562 100644 --- a/scripts/reinforcement_learning/skrl/play.py +++ b/scripts/reinforcement_learning/skrl/play.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -18,6 +13,7 @@ """Launch Isaac Sim Simulator first.""" import argparse +import sys from isaaclab.app import AppLauncher @@ -30,7 +26,17 @@ ) parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", + type=str, + default=None, + help=( + "Name of the RL agent configuration entry point. Defaults to None, in which case the argument " + "--algorithm is used to determine the default agent configuration entry point." + ), +) parser.add_argument("--checkpoint", type=str, default=None, help="Path to model checkpoint.") +parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--use_pretrained_checkpoint", action="store_true", @@ -54,11 +60,14 @@ # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) -args_cli = parser.parse_args() +# parse the arguments +args_cli, hydra_args = parser.parse_known_args() # always enable cameras to record video if args_cli.video: args_cli.enable_cameras = True +# clear out sys.argv for Hydra +sys.argv = [sys.argv[0]] + hydra_args # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -67,6 +76,7 @@ import gymnasium as gym import os +import random import time import torch @@ -74,7 +84,7 @@ from packaging import version # check for minimum supported skrl version -SKRL_VERSION = "1.4.1" +SKRL_VERSION = "1.4.3" if version.parse(skrl.__version__) < version.parse(SKRL_VERSION): skrl.logger.error( f"Unsupported skrl version: {skrl.__version__}. " @@ -87,7 +97,13 @@ elif args_cli.ml_framework.startswith("jax"): from skrl.utils.runner.jax import Runner -from isaaclab.envs import DirectMARLEnv, multi_agent_to_single_agent +from isaaclab.envs import ( + DirectMARLEnv, + DirectMARLEnvCfg, + DirectRLEnvCfg, + ManagerBasedRLEnvCfg, + multi_agent_to_single_agent, +) from isaaclab.utils.dict import print_dict from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint @@ -95,26 +111,43 @@ import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils import get_checkpoint_path, load_cfg_from_registry, parse_env_cfg +from isaaclab_tasks.utils import get_checkpoint_path +from uwlab_tasks.utils.hydra import hydra_task_config + +# PLACEHOLDER: Extension template (do not remove this comment) # config shortcuts -algorithm = args_cli.algorithm.lower() +if args_cli.agent is None: + algorithm = args_cli.algorithm.lower() + agent_cfg_entry_point = "skrl_cfg_entry_point" if algorithm in ["ppo"] else f"skrl_{algorithm}_cfg_entry_point" +else: + agent_cfg_entry_point = args_cli.agent + algorithm = agent_cfg_entry_point.split("_cfg")[0].split("skrl_")[-1].lower() -def main(): +@hydra_task_config(args_cli.task, agent_cfg_entry_point) +def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, experiment_cfg: dict): """Play with skrl agent.""" + # grab task name for checkpoint path + task_name = args_cli.task.split(":")[-1] + train_task_name = task_name.replace("-Play", "") + + # 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 + # configure the ML framework into the global skrl variable if args_cli.ml_framework.startswith("jax"): skrl.config.jax.backend = "jax" if args_cli.ml_framework == "jax" else "numpy" - # parse configuration - env_cfg = parse_env_cfg( - args_cli.task, device=args_cli.device, num_envs=args_cli.num_envs, use_fabric=not args_cli.disable_fabric - ) - try: - experiment_cfg = load_cfg_from_registry(args_cli.task, f"skrl_{algorithm}_cfg_entry_point") - except ValueError: - experiment_cfg = load_cfg_from_registry(args_cli.task, "skrl_cfg_entry_point") + # randomly sample a seed if seed = -1 + if args_cli.seed == -1: + args_cli.seed = random.randint(0, 10000) + + # set the agent and environment seed from command line + # note: certain randomization occur in the environment initialization so we set the seed here + experiment_cfg["seed"] = args_cli.seed if args_cli.seed is not None else experiment_cfg["seed"] + env_cfg.seed = experiment_cfg["seed"] # specify directory for logging experiments (load checkpoint) log_root_path = os.path.join("logs", "skrl", experiment_cfg["agent"]["experiment"]["directory"]) @@ -122,7 +155,7 @@ def main(): print(f"[INFO] Loading experiment from directory: {log_root_path}") # get checkpoint path if args_cli.use_pretrained_checkpoint: - resume_path = get_published_pretrained_checkpoint("skrl", args_cli.task) + resume_path = get_published_pretrained_checkpoint("skrl", train_task_name) if not resume_path: print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.") return @@ -134,6 +167,9 @@ def main(): ) log_dir = os.path.dirname(os.path.dirname(resume_path)) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) @@ -141,11 +177,11 @@ def main(): if isinstance(env.unwrapped, DirectMARLEnv) and algorithm in ["ppo"]: env = multi_agent_to_single_agent(env) - # get environment (physics) dt for real-time evaluation + # get environment (step) dt for real-time evaluation try: - dt = env.physics_dt + dt = env.step_dt except AttributeError: - dt = env.unwrapped.physics_dt + dt = env.unwrapped.step_dt # wrap for video recording if args_cli.video: diff --git a/scripts/reinforcement_learning/skrl/train.py b/scripts/reinforcement_learning/skrl/train.py index 83ad84c..33edecc 100644 --- a/scripts/reinforcement_learning/skrl/train.py +++ b/scripts/reinforcement_learning/skrl/train.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -29,12 +24,22 @@ parser.add_argument("--video_interval", type=int, default=2000, help="Interval between video recordings (in steps).") parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--agent", + type=str, + default=None, + help=( + "Name of the RL agent configuration entry point. Defaults to None, in which case the argument " + "--algorithm is used to determine the default agent configuration entry point." + ), +) parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--distributed", action="store_true", default=False, help="Run training with multiple GPUs or nodes." ) parser.add_argument("--checkpoint", type=str, default=None, help="Path to model checkpoint to resume training.") parser.add_argument("--max_iterations", type=int, default=None, help="RL Policy training iterations.") +parser.add_argument("--export_io_descriptors", action="store_true", default=False, help="Export IO descriptors.") parser.add_argument( "--ml_framework", type=str, @@ -49,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 @@ -68,6 +75,7 @@ """Rest everything follows.""" import gymnasium as gym +import logging import os import random from datetime import datetime @@ -76,7 +84,7 @@ from packaging import version # check for minimum supported skrl version -SKRL_VERSION = "1.4.1" +SKRL_VERSION = "1.4.3" if version.parse(skrl.__version__) < version.parse(SKRL_VERSION): skrl.logger.error( f"Unsupported skrl version: {skrl.__version__}. " @@ -98,17 +106,26 @@ ) from isaaclab.utils.assets import retrieve_file_path from isaaclab.utils.dict import print_dict -from isaaclab.utils.io import dump_pickle, dump_yaml +from isaaclab.utils.io import dump_yaml from isaaclab_rl.skrl import SkrlVecEnvWrapper import isaaclab_tasks # noqa: F401 import uwlab_tasks # noqa: F401 -from isaaclab_tasks.utils.hydra import hydra_task_config +from uwlab_tasks.utils.hydra import hydra_task_config + +# import logger +logger = logging.getLogger(__name__) + +# PLACEHOLDER: Extension template (do not remove this comment) # config shortcuts -algorithm = args_cli.algorithm.lower() -agent_cfg_entry_point = "skrl_cfg_entry_point" if algorithm in ["ppo"] else f"skrl_{algorithm}_cfg_entry_point" +if args_cli.agent is None: + algorithm = args_cli.algorithm.lower() + agent_cfg_entry_point = "skrl_cfg_entry_point" if algorithm in ["ppo"] else f"skrl_{algorithm}_cfg_entry_point" +else: + agent_cfg_entry_point = args_cli.agent + algorithm = agent_cfg_entry_point.split("_cfg")[0].split("skrl_")[-1].lower() @hydra_task_config(args_cli.task, agent_cfg_entry_point) @@ -118,6 +135,13 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen 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 + # check for invalid combination of CPU device with distributed training + if args_cli.distributed and args_cli.device is not None and "cpu" in args_cli.device: + raise ValueError( + "Distributed training is not supported when using CPU device. " + "Please use GPU device (e.g., --device cuda) for distributed training." + ) + # multi-gpu training config if args_cli.distributed: env_cfg.sim.device = f"cuda:{app_launcher.local_rank}" @@ -144,7 +168,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") + f"_{algorithm}_{args_cli.ml_framework}" - print(f"Exact experiment name requested from command line {log_dir}") + # 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"]}' # set directory into agent config @@ -156,12 +181,21 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # dump the configuration into log-directory dump_yaml(os.path.join(log_dir, "params", "env.yaml"), env_cfg) dump_yaml(os.path.join(log_dir, "params", "agent.yaml"), agent_cfg) - dump_pickle(os.path.join(log_dir, "params", "env.pkl"), env_cfg) - dump_pickle(os.path.join(log_dir, "params", "agent.pkl"), agent_cfg) # get checkpoint path (to resume training) resume_path = retrieve_file_path(args_cli.checkpoint) if args_cli.checkpoint else None + # set the IO descriptors export flag if requested + if isinstance(env_cfg, ManagerBasedRLEnvCfg): + env_cfg.export_io_descriptors = args_cli.export_io_descriptors + else: + logger.warning( + "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." + ) + + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/scripts/tools/blender_obj.py b/scripts/tools/blender_obj.py index 719aaeb..16b5513 100644 --- a/scripts/tools/blender_obj.py +++ b/scripts/tools/blender_obj.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/check_instanceable.py b/scripts/tools/check_instanceable.py index cf12dd6..f0da0e2 100644 --- a/scripts/tools/check_instanceable.py +++ b/scripts/tools/check_instanceable.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -73,6 +68,7 @@ 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 from isaaclab.utils import Timer from isaaclab.utils.assets import check_file_path @@ -87,6 +83,10 @@ def main(): sim = SimulationContext( stage_units_in_meters=1.0, physics_dt=0.01, rendering_dt=0.01, backend="torch", device="cuda:0" ) + + # get stage handle + stage = get_current_stage() + # enable fabric which avoids passing data over to USD structure # this speeds up the read-write operation of GPU buffers if sim.get_physics_context().use_gpu_pipeline: @@ -99,7 +99,7 @@ def main(): set_carb_setting(sim._settings, "/persistent/omnihydra/useSceneGraphInstancing", True) # Create interface to clone the scene - cloner = GridCloner(spacing=args_cli.spacing) + cloner = GridCloner(spacing=args_cli.spacing, stage=stage) cloner.define_base_env("/World/envs") prim_utils.define_prim("/World/envs/env_0") # Spawn things into stage diff --git a/scripts/tools/convert_instanceable.py b/scripts/tools/convert_instanceable.py new file mode 100644 index 0000000..73c2dd6 --- /dev/null +++ b/scripts/tools/convert_instanceable.py @@ -0,0 +1,161 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Utility to bulk convert URDFs or mesh files into instanceable USD format. + +Unified Robot Description Format (URDF) is an XML file format used in ROS to describe all elements of +a robot. For more information, see: http://wiki.ros.org/urdf + +This script uses the URDF importer extension from Isaac Sim (``omni.isaac.urdf_importer``) to convert a +URDF asset into USD format. It is designed as a convenience script for command-line use. For more +information on the URDF importer, see the documentation for the extension: +https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/ext_omni_isaac_urdf.html + + +positional arguments: + input The path to the input directory containing URDFs and Meshes. + output The path to directory to store the instanceable files. + +optional arguments: + -h, --help Show this help message and exit + --conversion-type Select file type to convert, urdf or mesh. (default: urdf) + --merge-joints Consolidate links that are connected by fixed joints. (default: False) + --fix-base Fix the base to where it is imported. (default: False) + --make-instanceable Make the asset instanceable for efficient cloning. (default: False) + +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Utility to convert a URDF or mesh into an Instanceable asset.") +parser.add_argument("input", type=str, help="The path to the input directory.") +parser.add_argument("output", type=str, help="The path to directory to store converted instanceable files.") +parser.add_argument( + "--conversion-type", type=str, default="both", help="Select file type to convert, urdf, mesh, or both." +) +parser.add_argument( + "--merge-joints", + action="store_true", + default=False, + help="Consolidate links that are connected by fixed joints.", +) +parser.add_argument("--fix-base", action="store_true", default=False, help="Fix the base to where it is imported.") +parser.add_argument( + "--make-instanceable", + action="store_true", + default=True, + help="Make the asset instanceable for efficient cloning.", +) +parser.add_argument( + "--collision-approximation", + type=str, + default="convexDecomposition", + choices=["convexDecomposition", "convexHull", "none"], + help=( + 'The method used for approximating collision mesh. Set to "none" ' + "to not add a collision mesh to the converted mesh." + ), +) +parser.add_argument( + "--mass", + type=float, + default=None, + help="The mass (in kg) to assign to the converted asset. If not provided, then no mass is added.", +) + +# 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 os + +from isaaclab.sim.converters import MeshConverter, MeshConverterCfg, UrdfConverter, UrdfConverterCfg +from isaaclab.sim.schemas import schemas_cfg + + +def main(): + + # Define conversion time given + conversion_type = args_cli.conversion_type.lower() + # Warning if conversion type input is not valid + if conversion_type != "urdf" and conversion_type != "mesh" and conversion_type != "both": + raise Warning("Conversion type is not valid, please select either 'urdf', 'mesh', or 'both'.") + + if not os.path.exists(args_cli.input): + print(f"Error: The directory {args_cli.input} does not exist.") + + # For each file and subsequent sub-directory + for root, dirs, files in os.walk(args_cli.input): + # For each file + for filename in files: + # Check for URDF extensions + if (conversion_type == "urdf" or conversion_type == "both") and filename.lower().endswith(".urdf"): + # URDF converter call + urdf_converter_cfg = UrdfConverterCfg( + asset_path=f"{root}/{filename}", + usd_dir=f"{args_cli.output}/{filename[:-5]}", + usd_file_name=f"{filename[:-5]}.usd", + fix_base=args_cli.fix_base, + merge_fixed_joints=args_cli.merge_joints, + force_usd_conversion=True, + make_instanceable=args_cli.make_instanceable, + ) + # Create Urdf converter and import the file + urdf_converter = UrdfConverter(urdf_converter_cfg) + print(f"Generated USD file: {urdf_converter.usd_path}") + + elif (conversion_type == "mesh" or conversion_type == "both") and ( + filename.lower().endswith(".fbx") + or filename.lower().endswith(".obj") + or filename.lower().endswith(".dae") + or filename.lower().endswith(".stl") + ): + # Mass properties + if args_cli.mass is not None: + mass_props = schemas_cfg.MassPropertiesCfg(mass=args_cli.mass) + rigid_props = schemas_cfg.RigidBodyPropertiesCfg() + else: + mass_props = None + rigid_props = None + + # Collision properties + collision_props = schemas_cfg.CollisionPropertiesCfg( + collision_enabled=args_cli.collision_approximation != "none" + ) + # Mesh converter call + mesh_converter_cfg = MeshConverterCfg( + mass_props=mass_props, + rigid_props=rigid_props, + collision_props=collision_props, + asset_path=f"{root}/{filename}", + force_usd_conversion=True, + usd_dir=f"{args_cli.output}/{filename[:-4]}", + usd_file_name=f"{filename[:-4]}.usd", + make_instanceable=args_cli.make_instanceable, + collision_approximation=args_cli.collision_approximation, + ) + # Create mesh converter and import the file + mesh_converter = MeshConverter(mesh_converter_cfg) + print(f"Generated USD file: {mesh_converter.usd_path}") + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts/tools/convert_mesh.py b/scripts/tools/convert_mesh.py index 85a7547..7139c62 100644 --- a/scripts/tools/convert_mesh.py +++ b/scripts/tools/convert_mesh.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -48,6 +43,18 @@ from isaaclab.app import AppLauncher +# Define collision approximation choices (must be defined before parser) +_valid_collision_approx = [ + "convexDecomposition", + "convexHull", + "triangleMesh", + "meshSimplification", + "sdf", + "boundingCube", + "boundingSphere", + "none", +] + # add argparse arguments parser = argparse.ArgumentParser(description="Utility to convert a mesh file into USD format.") parser.add_argument("input", type=str, help="The path to the input mesh file.") @@ -62,11 +69,8 @@ "--collision-approximation", type=str, default="convexDecomposition", - choices=["convexDecomposition", "convexHull", "none"], - help=( - 'The method used for approximating collision mesh. Set to "none" ' - "to not add a collision mesh to the converted mesh." - ), + choices=_valid_collision_approx, + help="The method used for approximating the collision mesh. Set to 'none' to disable collision mesh generation.", ) parser.add_argument( "--mass", @@ -97,6 +101,17 @@ from isaaclab.utils.assets import check_file_path from isaaclab.utils.dict import print_dict +collision_approximation_map = { + "convexDecomposition": schemas_cfg.ConvexDecompositionPropertiesCfg, + "convexHull": schemas_cfg.ConvexHullPropertiesCfg, + "triangleMesh": schemas_cfg.TriangleMeshPropertiesCfg, + "meshSimplification": schemas_cfg.TriangleMeshSimplificationPropertiesCfg, + "sdf": schemas_cfg.SDFMeshPropertiesCfg, + "boundingCube": schemas_cfg.BoundingCubePropertiesCfg, + "boundingSphere": schemas_cfg.BoundingSpherePropertiesCfg, + "none": None, +} + def main(): # check valid file path @@ -123,6 +138,15 @@ def main(): collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=args_cli.collision_approximation != "none") # Create Mesh converter config + cfg_class = collision_approximation_map.get(args_cli.collision_approximation) + if cfg_class is None and args_cli.collision_approximation != "none": + valid_keys = ", ".join(sorted(collision_approximation_map.keys())) + raise ValueError( + f"Invalid collision approximation type '{args_cli.collision_approximation}'. " + f"Valid options are: {valid_keys}." + ) + collision_cfg = cfg_class() if cfg_class is not None else None + mesh_converter_cfg = MeshConverterCfg( mass_props=mass_props, rigid_props=rigid_props, @@ -132,7 +156,7 @@ def main(): usd_dir=os.path.dirname(dest_path), usd_file_name=os.path.basename(dest_path), make_instanceable=args_cli.make_instanceable, - collision_approximation=args_cli.collision_approximation, + mesh_collision_props=collision_cfg, ) # Print info diff --git a/scripts/tools/convert_mjcf.py b/scripts/tools/convert_mjcf.py index 7c0f1a9..fa05a42 100644 --- a/scripts/tools/convert_mjcf.py +++ b/scripts/tools/convert_mjcf.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/convert_urdf.py b/scripts/tools/convert_urdf.py index 6145e5a..aaa5ddb 100644 --- a/scripts/tools/convert_urdf.py +++ b/scripts/tools/convert_urdf.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/cosmos/cosmos_prompt_gen.py b/scripts/tools/cosmos/cosmos_prompt_gen.py new file mode 100644 index 0000000..b0f3451 --- /dev/null +++ b/scripts/tools/cosmos/cosmos_prompt_gen.py @@ -0,0 +1,85 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Script to construct prompts to control the Cosmos model's generation. + +Required arguments: + --templates_path Path to the file containing templates for the prompts. + +Optional arguments: + --num_prompts Number of prompts to generate (default: 1). + --output_path Path to the output file to write generated prompts (default: prompts.txt). +""" + +import argparse +import json +import random + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Generate prompts for controlling Cosmos model's generation.") + parser.add_argument( + "--templates_path", type=str, required=True, help="Path to the JSON file containing prompt templates" + ) + parser.add_argument("--num_prompts", type=int, default=1, help="Number of prompts to generate (default: 1)") + parser.add_argument( + "--output_path", type=str, default="prompts.txt", help="Path to the output file to write generated prompts" + ) + args = parser.parse_args() + + return args + + +def generate_prompt(templates_path: str): + """Generate a random prompt for controlling the Cosmos model's visual augmentation. + + The prompt describes the scene and desired visual variations, which the model + uses to guide the augmentation process while preserving the core robotic actions. + + Args: + templates_path (str): Path to the JSON file containing prompt templates. + + Returns: + str: Generated prompt string that specifies visual aspects to modify in the video. + """ + try: + with open(templates_path) as f: + templates = json.load(f) + except FileNotFoundError: + raise FileNotFoundError(f"Prompt templates file not found: {templates_path}") + except json.JSONDecodeError: + raise ValueError(f"Invalid JSON in prompt templates file: {templates_path}") + + prompt_parts = [] + + for section_name, section_options in templates.items(): + if not isinstance(section_options, list): + continue + if len(section_options) == 0: + continue + selected_option = random.choice(section_options) + prompt_parts.append(selected_option) + + return " ".join(prompt_parts) + + +def main(): + # Parse command line arguments + args = parse_args() + + prompts = [generate_prompt(args.templates_path) for _ in range(args.num_prompts)] + + try: + with open(args.output_path, "w") as f: + for prompt in prompts: + f.write(prompt + "\n") + except Exception as e: + print(f"Failed to write to {args.output_path}: {e}") + + +if __name__ == "__main__": + main() diff --git a/scripts/tools/cosmos/transfer1_templates.json b/scripts/tools/cosmos/transfer1_templates.json new file mode 100644 index 0000000..d2d4b06 --- /dev/null +++ b/scripts/tools/cosmos/transfer1_templates.json @@ -0,0 +1,96 @@ +{ + "env": [ + "A robotic arm is picking up and stacking cubes inside a foggy industrial scrapyard at dawn, surrounded by piles of old robotic parts and twisted metal. The background includes large magnetic cranes, rusted conveyor belts, and flickering yellow floodlights struggling to penetrate the fog.", + "A robotic arm is picking up and stacking cubes inside a luxury penthouse showroom during sunset. The background includes minimalist designer furniture, a panoramic view of a glowing city skyline, and hovering autonomous drones offering refreshments.", + "A robotic arm is picking up and stacking cubes within an ancient temple-themed robotics exhibit in a museum. The background includes stone columns with hieroglyphic-style etchings, interactive display panels, and a few museum visitors observing silently from behind glass barriers.", + "A robotic arm is picking up and stacking cubes inside a futuristic daycare facility for children. The background includes robotic toys, soft padded walls, holographic storybooks floating in mid-air, and tiny humanoid robots assisting toddlers.", + "A robotic arm is picking up and stacking cubes inside a deep underwater laboratory where pressure-resistant glass panels reveal a shimmering ocean outside. The background includes jellyfish drifting outside the windows, robotic submarines gliding by, and walls lined with wet-surface equipment panels.", + "A robotic arm is picking up and stacking cubes inside a post-apocalyptic lab, partially collapsed and exposed to the open sky. The background includes ruined machinery, exposed rebar, and a distant city skyline covered in ash and fog.", + "A robotic arm is picking up and stacking cubes in a biotech greenhouse surrounded by lush plant life. The background includes rows of bio-engineered plants, misting systems, and hovering inspection drones checking crop health.", + "A robotic arm is picking up and stacking cubes inside a dark, volcanic research outpost. The background includes robotic arms encased in heat-resistant suits, seismic monitors, and distant lava fountains occasionally illuminating the space.", + "A robotic arm is picking up and stacking cubes inside an icy arctic base, with frost-covered walls and equipment glinting under bright artificial white lights. The background includes heavy-duty heaters, control consoles wrapped in thermal insulation, and a large window looking out onto a frozen tundra with polar winds swirling snow outside.", + "A robotic arm is picking up and stacking cubes inside a zero-gravity chamber on a rotating space habitat. The background includes floating lab instruments, panoramic windows showing stars and Earth in rotation, and astronauts monitoring data.", + "A robotic arm is picking up and stacking cubes inside a mystical tech-art installation blending robotics with generative art. The background includes sculptural robotics, shifting light patterns on the walls, and visitors interacting with the exhibit using gestures.", + "A robotic arm is picking up and stacking cubes in a Martian colony dome, under a terraformed red sky filtering through thick glass. The background includes pressure-locked entry hatches, Martian rovers parked outside, and domed hydroponic farms stretching into the distance.", + "A robotic arm is picking up and stacking cubes inside a high-security military robotics testing bunker, with matte green steel walls and strict order. The background includes surveillance cameras, camouflage netting over equipment racks, and military personnel observing from a secure glass-walled control room.", + "A robotic arm is picking up and stacking cubes inside a retro-futuristic robotics lab from the 1980s with checkered floors and analog computer panels. The background includes CRT monitors with green code, rotary dials, printed schematics on the walls, and operators in lab coats typing on clunky terminals.", + "A robotic arm is picking up and stacking cubes inside a sunken ancient ruin repurposed for modern robotics experiments. The background includes carved pillars, vines creeping through gaps in stone, and scattered crates of modern equipment sitting on ancient floors.", + "A robotic arm is picking up and stacking cubes on a luxury interstellar yacht cruising through deep space. The background includes elegant furnishings, ambient synth music systems, and holographic butlers attending to other passengers.", + "A robotic arm is picking up and stacking cubes in a rebellious underground cybernetic hacker hideout. The background includes graffiti-covered walls, tangled wires, makeshift workbenches, and anonymous figures hunched over terminals with scrolling code.", + "A robotic arm is picking up and stacking cubes inside a dense jungle outpost where technology is being tested in extreme organic environments. The background includes humid control panels, vines creeping onto the robotics table, and occasional wildlife observed from a distance by researchers in camo gear.", + "A robotic arm is picking up and stacking cubes in a minimalist Zen tech temple. The background includes bonsai trees on floating platforms, robotic monks sweeping floors silently, and smooth stone pathways winding through digital meditation alcoves." + ], + + "robot": [ + "The robot arm is matte dark green with yellow diagonal hazard stripes along the upper arm; the joints are rugged and chipped, and the hydraulics are exposed with faded red tubing.", + "The robot arm is worn orange with black caution tape markings near the wrist; the elbow joint is dented and the pistons have visible scarring from long use.", + "The robot arm is steel gray with smooth curved panels and subtle blue stripes running down the length; the joints are sealed tight and the hydraulics have a glossy black casing.", + "The robot arm is bright yellow with alternating black bands around each segment; the joints show minor wear, and the hydraulics gleam with fresh lubrication.", + "The robot arm is navy blue with white serial numbers stenciled along the arm; the joints are well-maintained and the hydraulic shafts are matte silver with no visible dirt.", + "The robot arm is deep red with a matte finish and faint white grid lines across the panels; the joints are squared off and the hydraulic units look compact and embedded.", + "The robot arm is dirty white with dark gray speckled patches from wear; the joints are squeaky with exposed rivets, and the hydraulics are rusted at the base.", + "The robot arm is olive green with chipped paint and a black triangle warning icon near the shoulder; the joints are bulky and the hydraulics leak slightly around the seals.", + "The robot arm is bright teal with a glossy surface and silver stripes on the outer edges; the joints rotate smoothly and the pistons reflect a pale cyan hue.", + "The robot arm is orange-red with carbon fiber textures and white racing-style stripes down the forearm; the joints have minimal play and the hydraulics are tightly sealed in synthetic tubing.", + "The robot arm is flat black with uneven camouflage blotches in dark gray; the joints are reinforced and the hydraulic tubes are dusty and loose-fitting.", + "The robot arm is dull maroon with vertical black grooves etched into the panels; the joints show corrosion on the bolts and the pistons are thick and slow-moving.", + "The robot arm is powder blue with repeating geometric patterns printed in light gray; the joints are square and the hydraulic systems are internal and silent.", + "The robot arm is brushed silver with high-gloss finish and blue LED strips along the seams; the joints are shiny and tight, and the hydraulics hiss softly with every movement.", + "The robot arm is lime green with paint faded from sun exposure and white warning labels near each joint; the hydraulics are scraped and the fittings show heat marks.", + "The robot arm is dusty gray with chevron-style black stripes pointing toward the claw; the joints have uneven wear, and the pistons are dented and slightly bent.", + "The robot arm is cobalt blue with glossy texture and stylized angular black patterns across each segment; the joints are clean and the hydraulics show new flexible tubing.", + "The robot arm is industrial brown with visible welded seams and red caution tape wrapped loosely around the middle section; the joints are clunky and the hydraulics are slow and loud.", + "The robot arm is flat tan with dark green splotches and faint stencil text across the forearm; the joints have dried mud stains and the pistons are partially covered in grime.", + "The robot arm is light orange with chrome hexagon detailing and black number codes on the side; the joints are smooth and the hydraulic actuators shine under the lab lights." + ], + + "table": [ + "The robot arm is mounted on a table that is dull gray metal with scratches and scuff marks across the surface; faint rust rings are visible where older machinery used to be mounted.", + "The robot arm is mounted on a table that is smooth black plastic with a matte finish and faint fingerprint smudges near the edges; corners are slightly worn from regular use.", + "The robot arm is mounted on a table that is light oak wood with a natural grain pattern and a glossy varnish that reflects overhead lights softly; small burn marks dot one corner.", + "The robot arm is mounted on a table that is rough concrete with uneven texture and visible air bubbles; some grease stains and faded yellow paint markings suggest heavy usage.", + "The robot arm is mounted on a table that is brushed aluminum with a clean silver tone and very fine linear grooves; surface reflects light evenly, giving a soft glow.", + "The robot arm is mounted on a table that is pale green composite with chipped corners and scratches revealing darker material beneath; tape residue is stuck along the edges.", + "The robot arm is mounted on a table that is dark brown with a slightly cracked synthetic coating; patches of discoloration suggest exposure to heat or chemicals over time.", + "The robot arm is mounted on a table that is polished steel with mirror-like reflections; every small movement of the robot is mirrored faintly across the surface.", + "The robot arm is mounted on a table that is white with a slightly textured ceramic top, speckled with tiny black dots; the surface is clean but the edges are chipped.", + "The robot arm is mounted on a table that is glossy black glass with a deep shine and minimal dust; any lights above are clearly reflected, and fingerprints are visible under certain angles.", + "The robot arm is mounted on a table that is matte red plastic with wide surface scuffs and paint transfer from other objects; faint gridlines are etched into one side.", + "The robot arm is mounted on a table that is dark navy laminate with a low-sheen surface and subtle wood grain texture; the edge banding is slightly peeling off.", + "The robot arm is mounted on a table that is yellow-painted steel with diagonal black warning stripes running along one side; the paint is scratched and faded in high-contact areas.", + "The robot arm is mounted on a table that is translucent pale blue polymer with internal striations and slight glow under overhead lights; small bubbles are frozen inside the material.", + "The robot arm is mounted on a table that is cold concrete with embedded metal panels bolted into place; the surface has oil stains, welding marks, and tiny debris scattered around.", + "The robot arm is mounted on a table that is shiny chrome with heavy smudging and streaks; the table reflects distorted shapes of everything around it, including the arm itself.", + "The robot arm is mounted on a table that is matte forest green with shallow dents and drag marks from prior mechanical operations; a small sticker label is half-torn in one corner.", + "The robot arm is mounted on a table that is textured black rubber with slight give under pressure; scratches from the robot's base and clamp marks are clearly visible.", + "The robot arm is mounted on a table that is medium gray ceramic tile with visible grout lines and chips along the edges; some tiles have tiny cracks or stains.", + "The robot arm is mounted on a table that is old dark wood with faded polish and visible circular stains from spilled liquids; a few deep grooves are carved into the surface near the center." + ], + + "cubes": [ + "The arm is connected to the base mounted on the table. The bottom cube is deep blue, the second cube is bright red, and the top cube is vivid green, maintaining their correct order after stacking." + ], + + "light": [ + "The lighting is soft and diffused from large windows, allowing daylight to fill the room, creating gentle shadows that elongate throughout the space, with a natural warmth due to the sunlight streaming in.", + "Bright fluorescent tubes overhead cast a harsh, even light across the scene, creating sharp, well-defined shadows under the arm and cubes, with a sterile, clinical feel due to the cold white light.", + "Warm tungsten lights in the ceiling cast a golden glow over the table, creating long, soft shadows and a cozy, welcoming atmosphere. The light contrasts with cool blue tones from the robot arm.", + "The lighting comes from several intense spotlights mounted above, each casting focused beams of light that create stark, dramatic shadows around the cubes and the robotic arm, producing a high-contrast look.", + "A single adjustable desk lamp with a soft white bulb casts a directional pool of light over the cubes, causing deep, hard shadows and a quiet, intimate feel in the dimly lit room.", + "The space is illuminated with bright daylight filtering in through a skylight above, casting diffused, soft shadows and giving the scene a clean and natural look, with a cool tint from the daylight.", + "Soft, ambient lighting from hidden LEDs embedded in the ceiling creates a halo effect around the robotic arm, while subtle, elongated shadows stretch across the table surface, giving a sleek modern vibe.", + "Neon strip lights line the walls, casting a cool blue and purple glow across the scene. The robot and table are bathed in this colored light, producing sharp-edged shadows with a futuristic feel.", + "Bright artificial lights overhead illuminate the scene in a harsh white, with scattered, uneven shadows across the table and robot arm. There's a slight yellow hue to the light, giving it an industrial ambiance.", + "Soft morning sunlight spills through a large open window, casting long shadows across the floor and the robot arm. The warm, golden light creates a peaceful, natural atmosphere with a slight coolness in the shadows.", + "Dim ambient lighting with occasional flashes of bright blue light from overhead digital screens creates a high-tech, slightly eerie atmosphere. The shadows are soft, stretching in an almost surreal manner.", + "Lighting from tall lamps outside the room filters in through large glass doors, casting angled shadows across the table and robot arm. The ambient light creates a relaxing, slightly diffused atmosphere.", + "Artificial overhead lighting casts a harsh, stark white light with little warmth, producing sharply defined, almost clinical shadows on the robot arm and cubes. The space feels cold and industrial.", + "Soft moonlight from a large window at night creates a cool, ethereal glow on the table and arm. The shadows are long and faint, and the lighting provides a calm and serene atmosphere.", + "Bright overhead LED panels illuminate the scene with clean, white light, casting neutral shadows that give the environment a modern, sleek feel with minimal distortion or softness in the shadows.", + "A floodlight positioned outside casts bright, almost blinding natural light through an open door, creating high-contrast, sharp-edged shadows across the table and robot arm, adding dramatic tension to the scene.", + "Dim lighting from vintage tungsten bulbs hanging from the ceiling gives the room a warm, nostalgic glow, casting elongated, soft shadows that provide a cozy atmosphere around the robotic arm.", + "Bright fluorescent lights directly above produce a harsh, clinical light that creates sharp, defined shadows on the table and robotic arm, enhancing the industrial feel of the scene.", + "Neon pink and purple lights flicker softly from the walls, illuminating the robot arm with an intense glow that produces sharp, angular shadows across the cubes. The atmosphere feels futuristic and edgy.", + "Sunlight pouring in from a large, open window bathes the table and robotic arm in a warm golden light. The shadows are soft, and the scene feels natural and inviting with a slight contrast between light and shadow." + ] +} diff --git a/scripts/tools/hdf5_to_mp4.py b/scripts/tools/hdf5_to_mp4.py new file mode 100644 index 0000000..98fc1a9 --- /dev/null +++ b/scripts/tools/hdf5_to_mp4.py @@ -0,0 +1,209 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Script to convert HDF5 demonstration files to MP4 videos. + +This script converts camera frames stored in HDF5 demonstration files to MP4 videos. +It supports multiple camera modalities including RGB, segmentation, and normal maps. +The output videos are saved in the specified directory with appropriate naming. + +required arguments: + --input_file Path to the input HDF5 file. + --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"]) + --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 + +# Constants +DEFAULT_VIDEO_HEIGHT = 704 +DEFAULT_VIDEO_WIDTH = 1280 +DEFAULT_INPUT_KEYS = [ + "table_cam", + "wrist_cam", + "table_cam_segmentation", + "table_cam_normals", + "table_cam_shaded_segmentation", + "table_cam_depth", +] +DEFAULT_FRAMERATE = 30 +LIGHT_SOURCE = np.array([0.0, 0.0, 1.0]) +MIN_DEPTH = 0.0 +MAX_DEPTH = 1.5 + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Convert HDF5 demonstration files to MP4 videos.") + parser.add_argument( + "--input_file", + type=str, + required=True, + help="Path to the input HDF5 file containing demonstration data.", + ) + parser.add_argument( + "--output_dir", + type=str, + required=True, + help="Directory path where the output MP4 files will be saved.", + ) + + parser.add_argument( + "--input_keys", + type=str, + nargs="+", + default=DEFAULT_INPUT_KEYS, + help="List of input keys to process.", + ) + parser.add_argument( + "--video_height", + type=int, + default=DEFAULT_VIDEO_HEIGHT, + help="Height of the output video in pixels.", + ) + parser.add_argument( + "--video_width", + type=int, + default=DEFAULT_VIDEO_WIDTH, + help="Width of the output video in pixels.", + ) + parser.add_argument( + "--framerate", + type=int, + default=DEFAULT_FRAMERATE, + help="Frames per second for the output video.", + ) + + args = parser.parse_args() + + return args + + +def write_demo_to_mp4( + hdf5_file, + demo_id, + frames_path, + input_key, + output_dir, + video_height, + video_width, + framerate=DEFAULT_FRAMERATE, +): + """Convert frames from an HDF5 file to an MP4 video. + + Args: + hdf5_file (str): Path to the HDF5 file containing the frames. + demo_id (int): ID of the demonstration to convert. + frames_path (str): Path to the frames data in the HDF5 file. + input_key (str): Name of the input key to convert. + output_dir (str): Directory to save the output MP4 file. + video_height (int): Height of the output video in pixels. + video_width (int): Width of the output video in pixels. + framerate (int, optional): Frames per second for the output video. Defaults to 30. + """ + with h5py.File(hdf5_file, "r") as f: + # Get frames based on input key type + if "shaded_segmentation" in input_key: + temp_key = input_key.replace("shaded_segmentation", "segmentation") + frames = f[f"data/demo_{demo_id}/obs/{temp_key}"] + else: + frames = f[frames_path + "/" + input_key] + + # Setup video writer + output_path = os.path.join(output_dir, f"demo_{demo_id}_{input_key}.mp4") + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + if "depth" in input_key: + video = cv2.VideoWriter(output_path, fourcc, framerate, (video_width, video_height), isColor=False) + else: + video = cv2.VideoWriter(output_path, fourcc, framerate, (video_width, video_height)) + + # Process and write frames + for ix, frame in enumerate(frames): + # Convert normal maps to uint8 if needed + if "normals" in input_key: + frame = (frame * 255.0).astype(np.uint8) + + # Process shaded segmentation frames + elif "shaded_segmentation" in input_key: + seg = frame[..., :-1] + normals_key = input_key.replace("shaded_segmentation", "normals") + normals = f[f"data/demo_{demo_id}/obs/{normals_key}"][ix] + shade = 0.5 + (normals * LIGHT_SOURCE[None, None, :]).sum(axis=-1) * 0.5 + shaded_seg = (shade[..., None] * seg).astype(np.uint8) + frame = np.concatenate((shaded_seg, frame[..., -1:]), axis=-1) + + # Convert RGB to BGR + if "depth" not in input_key: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + else: + frame = (frame[..., 0] - MIN_DEPTH) / (MAX_DEPTH - MIN_DEPTH) + frame = np.where(frame < 0.01, 1.0, frame) + frame = 1.0 - frame + frame = (frame * 255.0).astype(np.uint8) + + # Resize to video resolution + frame = cv2.resize(frame, (video_width, video_height), interpolation=cv2.INTER_CUBIC) + video.write(frame) + + video.release() + + +def get_num_demos(hdf5_file): + """Get the number of demonstrations in the HDF5 file. + + Args: + hdf5_file (str): Path to the HDF5 file. + + Returns: + int: Number of demonstrations found in the file. + """ + with h5py.File(hdf5_file, "r") as f: + return len(f["data"].keys()) + + +def main(): + """Main function to convert all demonstrations to MP4 videos.""" + # Parse command line arguments + args = parse_args() + + # Create output directory if it doesn't exist + os.makedirs(args.output_dir, exist_ok=True) + + # Get number of demonstrations from the file + num_demos = get_num_demos(args.input_file) + print(f"Found {num_demos} demonstrations in {args.input_file}") + + # Convert each demonstration + for i in range(num_demos): + frames_path = f"data/demo_{str(i)}/obs" + for input_key in args.input_keys: + write_demo_to_mp4( + args.input_file, + i, + frames_path, + input_key, + args.output_dir, + args.video_height, + args.video_width, + args.framerate, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/tools/merge_hdf5_datasets.py b/scripts/tools/merge_hdf5_datasets.py index 774e205..92329f7 100644 --- a/scripts/tools/merge_hdf5_datasets.py +++ b/scripts/tools/merge_hdf5_datasets.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/mp4_to_hdf5.py b/scripts/tools/mp4_to_hdf5.py new file mode 100644 index 0000000..a4d8f43 --- /dev/null +++ b/scripts/tools/mp4_to_hdf5.py @@ -0,0 +1,172 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Script to create a new dataset by combining existing HDF5 demonstrations with visually augmented MP4 videos. + +This script takes an existing HDF5 dataset containing demonstrations and a directory of MP4 videos +that are visually augmented versions of the original demonstration videos (e.g., with different lighting, +color schemes, or visual effects). It creates a new HDF5 dataset that preserves all the original +demonstration data (actions, robot state, etc.) but replaces the video frames with the augmented versions. + +required arguments: + --input_file Path to the input HDF5 file containing original demonstrations. + --output_file Path to save the new HDF5 file with augmented videos. + --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 + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Create a new dataset with visually augmented videos.") + parser.add_argument( + "--input_file", + type=str, + required=True, + help="Path to the input HDF5 file containing original demonstrations.", + ) + parser.add_argument( + "--videos_dir", + type=str, + required=True, + help="Directory containing the visually augmented MP4 videos.", + ) + parser.add_argument( + "--output_file", + type=str, + required=True, + help="Path to save the new HDF5 file with augmented videos.", + ) + + args = parser.parse_args() + + return args + + +def get_frames_from_mp4(video_path, target_height=None, target_width=None): + """Extract frames from an MP4 video file. + + Args: + video_path (str): Path to the MP4 video file. + target_height (int, optional): Target height for resizing frames. If None, no resizing is done. + target_width (int, optional): Target width for resizing frames. If None, no resizing is done. + + Returns: + np.ndarray: Array of frames from the video in RGB format. + """ + # Open the video file + video = cv2.VideoCapture(video_path) + + # Get video properties + frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + + # Read all frames into a numpy array + frames = [] + for _ in range(frame_count): + ret, frame = video.read() + if not ret: + break + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if target_height is not None and target_width is not None: + frame = cv2.resize(frame, (target_width, target_height), interpolation=cv2.INTER_LINEAR) + frames.append(frame) + + # Convert to numpy array + frames = np.array(frames).astype(np.uint8) + + # Release the video object + video.release() + + return frames + + +def process_video_and_demo(f_in, f_out, video_path, orig_demo_id, new_demo_id): + """Process a single video and create a new demo with augmented video frames. + + Args: + f_in (h5py.File): Input HDF5 file. + f_out (h5py.File): Output HDF5 file. + video_path (str): Path to the augmented video file. + orig_demo_id (int): ID of the original demo to copy. + new_demo_id (int): ID for the new demo. + """ + # Get original demo data + actions = f_in[f"data/demo_{str(orig_demo_id)}/actions"] + eef_pos = f_in[f"data/demo_{str(orig_demo_id)}/obs/eef_pos"] + eef_quat = f_in[f"data/demo_{str(orig_demo_id)}/obs/eef_quat"] + gripper_pos = f_in[f"data/demo_{str(orig_demo_id)}/obs/gripper_pos"] + wrist_cam = f_in[f"data/demo_{str(orig_demo_id)}/obs/wrist_cam"] + + # Get original video resolution + orig_video = f_in[f"data/demo_{str(orig_demo_id)}/obs/table_cam"] + target_height, target_width = orig_video.shape[1:3] + + # Extract frames from video with original resolution + frames = get_frames_from_mp4(video_path, target_height, target_width) + + # Create new datasets + f_out.create_dataset(f"data/demo_{str(new_demo_id)}/actions", data=actions, compression="gzip") + f_out.create_dataset(f"data/demo_{str(new_demo_id)}/obs/eef_pos", data=eef_pos, compression="gzip") + f_out.create_dataset(f"data/demo_{str(new_demo_id)}/obs/eef_quat", data=eef_quat, compression="gzip") + f_out.create_dataset(f"data/demo_{str(new_demo_id)}/obs/gripper_pos", data=gripper_pos, compression="gzip") + f_out.create_dataset( + f"data/demo_{str(new_demo_id)}/obs/table_cam", data=frames.astype(np.uint8), compression="gzip" + ) + f_out.create_dataset(f"data/demo_{str(new_demo_id)}/obs/wrist_cam", data=wrist_cam, compression="gzip") + + # Copy attributes + f_out[f"data/demo_{str(new_demo_id)}"].attrs["num_samples"] = f_in[f"data/demo_{str(orig_demo_id)}"].attrs[ + "num_samples" + ] + + +def main(): + """Main function to create a new dataset with augmented videos.""" + # Parse command line arguments + args = parse_args() + + # Get list of MP4 videos + search_path = os.path.join(args.videos_dir, "*.mp4") + video_paths = glob.glob(search_path) + video_paths.sort() + print(f"Found {len(video_paths)} MP4 videos in {args.videos_dir}") + + # Create output directory if it doesn't exist + os.makedirs(os.path.dirname(args.output_file), exist_ok=True) + + with h5py.File(args.input_file, "r") as f_in, h5py.File(args.output_file, "w") as f_out: + # Copy all data from input to output + f_in.copy("data", f_out) + + # Get the largest demo ID to start new demos from + demo_ids = [int(key.split("_")[1]) for key in f_in["data"].keys()] + next_demo_id = max(demo_ids) + 1 # noqa: SIM113 + print(f"Starting new demos from ID: {next_demo_id}") + + # Process each video and create new demo + for video_path in video_paths: + # Extract original demo ID from video filename + video_filename = os.path.basename(video_path) + orig_demo_id = int(video_filename.split("_")[1]) + + process_video_and_demo(f_in, f_out, video_path, orig_demo_id, next_demo_id) + next_demo_id += 1 + + print(f"Augmented data saved to {args.output_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/tools/pretrained_checkpoint.py b/scripts/tools/pretrained_checkpoint.py index 74788e7..96b8562 100644 --- a/scripts/tools/pretrained_checkpoint.py +++ b/scripts/tools/pretrained_checkpoint.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/process_meshes_to_obj.py b/scripts/tools/process_meshes_to_obj.py index d274496..331f7b0 100644 --- a/scripts/tools/process_meshes_to_obj.py +++ b/scripts/tools/process_meshes_to_obj.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/scripts/tools/record_demos.py b/scripts/tools/record_demos.py index 257042d..e3609d3 100644 --- a/scripts/tools/record_demos.py +++ b/scripts/tools/record_demos.py @@ -1,13 +1,7 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - """ Script to record demonstrations with Isaac Lab environments using human teleoperation. @@ -29,15 +23,26 @@ """Launch Isaac Sim Simulator first.""" +# Standard library imports import argparse -import os +import contextlib +# Isaac Lab AppLauncher from isaaclab.app import AppLauncher # add argparse arguments parser = argparse.ArgumentParser(description="Record demonstrations for Isaac Lab environments.") -parser.add_argument("--task", type=str, default=None, help="Name of the task.") -parser.add_argument("--teleop_device", type=str, default="keyboard", help="Device for interacting with environment.") +parser.add_argument("--task", type=str, required=True, help="Name of the task.") +parser.add_argument( + "--teleop_device", + type=str, + default="keyboard", + help=( + "Teleop device. Set here (legacy) or via the environment config. If using the environment config, pass the" + " device key/name defined under 'teleop_devices' (it can be a custom name, not necessarily 'handtracking')." + " Built-ins: keyboard, spacemouse, gamepad. Not all tasks support all built-ins." + ), +) parser.add_argument( "--dataset_file", type=str, default="./datasets/dataset.hdf5", help="File path to export recorded demos." ) @@ -51,13 +56,30 @@ default=10, help="Number of continuous steps with task success for concluding a demo as successful. Default is 10.", ) +parser.add_argument( + "--enable_pinocchio", + action="store_true", + default=False, + help="Enable Pinocchio.", +) + # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments args_cli = parser.parse_args() -if args_cli.teleop_device.lower() == "handtracking": - vars(args_cli)["experience"] = f'{os.environ["ISAACLAB_PATH"]}/apps/isaaclab.python.xr.openxr.kit' +# Validate required arguments +if args_cli.task is None: + parser.error("--task is required") + +app_launcher_args = vars(args_cli) + +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim + # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter + import pinocchio # noqa: F401 +if "handtracking" in args_cli.teleop_device.lower(): + app_launcher_args["xr"] = True # launch the simulator app_launcher = AppLauncher(args_cli) @@ -65,38 +87,62 @@ """Rest everything follows.""" -import contextlib + +# Third-party imports import gymnasium as gym +import logging +import os import time import torch -import omni.log +import omni.ui as ui + +from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg, Se3SpaceMouse, Se3SpaceMouseCfg +from isaaclab.devices.openxr import remove_camera_configs +from isaaclab.devices.teleop_device_factory import create_teleop_device + +import isaaclab_mimic.envs # noqa: F401 +from isaaclab_mimic.ui.instruction_display import InstructionDisplay, show_subtask_instructions + +if args_cli.enable_pinocchio: + import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 + import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 + +from collections.abc import Callable -from isaaclab.devices import Se3HandTracking, Se3Keyboard, Se3SpaceMouse -from isaaclab.envs import ViewerCfg +from isaaclab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg -from isaaclab.envs.ui import ViewportCameraController +from isaaclab.envs.ui import EmptyWindow +from isaaclab.managers import DatasetExportMode import isaaclab_tasks # noqa: F401 import uwlab_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.""" - def __init__(self, hz): - """ + def __init__(self, hz: int): + """Initialize a RateLimiter with specified frequency. + Args: - hz (int): frequency to enforce + hz: Frequency to enforce in Hertz. """ self.hz = hz self.last_time = time.time() self.sleep_duration = 1.0 / hz self.render_period = min(0.033, self.sleep_duration) - def sleep(self, env): - """Attempt to sleep at the specified rate in hz.""" + def sleep(self, env: gym.Env): + """Attempt to sleep at the specified rate in hz. + + Args: + env: Environment to render during sleep periods. + """ next_wakeup_time = self.last_time + self.sleep_duration while time.time() < next_wakeup_time: time.sleep(self.render_period) @@ -110,30 +156,17 @@ def sleep(self, env): self.last_time += self.sleep_duration -def pre_process_actions(delta_pose: torch.Tensor, gripper_command: bool) -> torch.Tensor: - """Pre-process actions for the environment.""" - # compute actions based on environment - if "Reach" in args_cli.task: - # note: reach is the only one that uses a different action space - # compute actions - return delta_pose - else: - # resolve gripper command - gripper_vel = torch.zeros((delta_pose.shape[0], 1), dtype=torch.float, device=delta_pose.device) - gripper_vel[:] = -1 if gripper_command else 1 - # compute actions - return torch.concat([delta_pose, gripper_vel], dim=1) - +def setup_output_directories() -> tuple[str, str]: + """Set up output directories for saving demonstrations. -def main(): - """Collect demonstrations from the environment using teleop interfaces.""" - - # if handtracking is selected, rate limiting is achieved via OpenXR - if args_cli.teleop_device.lower() == "handtracking": - rate_limiter = None - else: - rate_limiter = RateLimiter(args_cli.step_hz) + Creates the output directory if it doesn't exist and extracts the file name + from the dataset file path. + Returns: + tuple[str, str]: A tuple containing: + - output_dir: The directory path where the dataset will be saved + - output_file_name: The filename (without extension) for the dataset + """ # get directory path and file name (without extension) from cli arguments output_dir = os.path.dirname(args_cli.dataset_file) output_file_name = os.path.splitext(os.path.basename(args_cli.dataset_file))[0] @@ -141,10 +174,38 @@ def main(): # create directory if it does not exist if not os.path.exists(output_dir): os.makedirs(output_dir) + print(f"Created output directory: {output_dir}") + + return output_dir, output_file_name + + +def create_environment_config( + output_dir: str, output_file_name: str +) -> tuple[ManagerBasedRLEnvCfg | DirectRLEnvCfg, object | None]: + """Create and configure the environment configuration. + + Parses the environment configuration and makes necessary adjustments for demo recording. + Extracts the success termination function and configures the recorder manager. + + Args: + output_dir: Directory where recorded demonstrations will be saved + output_file_name: Name of the file to store the demonstrations + + Returns: + tuple[isaaclab_tasks.utils.parse_cfg.EnvCfg, Optional[object]]: A tuple containing: + - env_cfg: The configured environment configuration + - success_term: The success termination object or None if not available + Raises: + Exception: If parsing the environment configuration fails + """ # parse configuration - env_cfg = parse_env_cfg(args_cli.task, device=args_cli.device, num_envs=1) - env_cfg.env_name = args_cli.task + try: + 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: + logger.error(f"Failed to parse environment configuration: {e}") + exit(1) # extract success checking function to invoke in the main loop success_term = None @@ -152,106 +213,346 @@ def main(): 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." ) + if args_cli.xr: + # If cameras are not enabled and XR is enabled, remove camera configs + if not args_cli.enable_cameras: + env_cfg = remove_camera_configs(env_cfg) + env_cfg.sim.render.antialiasing_mode = "DLSS" + # modify configuration such that the environment runs indefinitely until # the goal is reached or other termination conditions are met env_cfg.terminations.time_out = None - env_cfg.observations.policy.concatenate_terms = False env_cfg.recorders: ActionStateRecorderManagerCfg = ActionStateRecorderManagerCfg() env_cfg.recorders.dataset_export_dir_path = output_dir env_cfg.recorders.dataset_filename = output_file_name + env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_SUCCEEDED_ONLY + + return env_cfg, success_term + + +def create_environment(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg) -> gym.Env: + """Create the environment from the configuration. + + Args: + env_cfg: The environment configuration object that defines the environment properties. + This should be an instance of EnvCfg created by parse_env_cfg(). + + Returns: + gym.Env: A Gymnasium environment instance for the specified task. + + Raises: + Exception: If environment creation fails for any reason. + """ + try: + env = gym.make(args_cli.task, cfg=env_cfg).unwrapped + return env + except Exception as e: + logger.error(f"Failed to create environment: {e}") + exit(1) + + +def setup_teleop_device(callbacks: dict[str, Callable]) -> object: + """Set up the teleoperation device based on configuration. + + Attempts to create a teleoperation device based on the environment configuration. + Falls back to default devices if the specified device is not found in the configuration. + + Args: + callbacks: Dictionary mapping callback keys to functions that will be + attached to the teleop device + + Returns: + object: The configured teleoperation device interface + + Raises: + Exception: If teleop device creation fails + """ + teleop_interface = None + try: + if hasattr(env_cfg, "teleop_devices") and args_cli.teleop_device in env_cfg.teleop_devices.devices: + teleop_interface = create_teleop_device(args_cli.teleop_device, env_cfg.teleop_devices.devices, callbacks) + else: + logger.warning( + f"No teleop device '{args_cli.teleop_device}' found in environment config. Creating default." + ) + # Create fallback teleop device + if args_cli.teleop_device.lower() == "keyboard": + teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) + elif args_cli.teleop_device.lower() == "spacemouse": + teleop_interface = Se3SpaceMouse(Se3SpaceMouseCfg(pos_sensitivity=0.2, rot_sensitivity=0.5)) + else: + logger.error(f"Unsupported teleop device: {args_cli.teleop_device}") + logger.error("Supported devices: keyboard, spacemouse, handtracking") + exit(1) + + # Add callbacks to fallback device + for key, callback in callbacks.items(): + teleop_interface.add_callback(key, callback) + except Exception as e: + logger.error(f"Failed to create teleop device: {e}") + exit(1) + + if teleop_interface is None: + logger.error("Failed to create teleop interface") + exit(1) + + return teleop_interface + + +def setup_ui(label_text: str, env: gym.Env) -> InstructionDisplay: + """Set up the user interface elements. + + Creates instruction display and UI window with labels for showing information + to the user during demonstration recording. + + Args: + label_text: Text to display showing current recording status + env: The environment instance for which UI is being created + + Returns: + InstructionDisplay: The configured instruction display object + """ + instruction_display = InstructionDisplay(args_cli.xr) + if not args_cli.xr: + window = EmptyWindow(env, "Instruction") + with window.ui_window_elements["main_vstack"]: + demo_label = ui.Label(label_text) + subtask_label = ui.Label("") + instruction_display.set_labels(subtask_label, demo_label) + + return instruction_display + + +def process_success_condition(env: gym.Env, success_term: object | None, success_step_count: int) -> tuple[int, bool]: + """Process the success condition for the current step. + + Checks if the environment has met the success condition for the required + number of consecutive steps. Marks the episode as successful if criteria are met. + + Args: + env: The environment instance to check + success_term: The success termination object or None if not available + success_step_count: Current count of consecutive successful steps + + Returns: + tuple[int, bool]: A tuple containing: + - updated success_step_count: The updated count of consecutive successful steps + - success_reset_needed: Boolean indicating if reset is needed due to success + """ + if success_term is None: + return success_step_count, False + + if bool(success_term.func(env, **success_term.params)[0]): + success_step_count += 1 + if success_step_count >= args_cli.num_success_steps: + env.recorder_manager.record_pre_reset([0], force_export_or_skip=False) + env.recorder_manager.set_success_to_episodes( + [0], torch.tensor([[True]], dtype=torch.bool, device=env.device) + ) + env.recorder_manager.export_episodes([0]) + print("Success condition met! Recording completed.") + return success_step_count, True + else: + success_step_count = 0 + + return success_step_count, False + - # create environment - env = gym.make(args_cli.task, cfg=env_cfg).unwrapped +def handle_reset( + env: gym.Env, success_step_count: int, instruction_display: InstructionDisplay, label_text: str +) -> int: + """Handle resetting the environment. - # add teleoperation key for reset current recording instance + Resets the environment, recorder manager, and related state variables. + Updates the instruction display with current status. + + Args: + env: The environment instance to reset + success_step_count: Current count of consecutive successful steps + instruction_display: The display object to update + label_text: Text to display showing current recording status + + Returns: + int: Reset success step count (0) + """ + print("Resetting environment...") + env.sim.reset() + env.recorder_manager.reset() + env.reset() + success_step_count = 0 + instruction_display.show_demo(label_text) + return success_step_count + + +def run_simulation_loop( + env: gym.Env, + teleop_interface: object | None, + success_term: object | None, + rate_limiter: RateLimiter | None, +) -> int: + """Run the main simulation loop for collecting demonstrations. + + Sets up callback functions for teleop device, initializes the UI, + and runs the main loop that processes user inputs and environment steps. + Records demonstrations when success conditions are met. + + Args: + env: The environment instance + teleop_interface: Optional teleop interface (will be created if None) + success_term: The success termination object or None if not available + rate_limiter: Optional rate limiter to control simulation speed + + Returns: + int: Number of successful demonstrations recorded + """ + current_recorded_demo_count = 0 + success_step_count = 0 should_reset_recording_instance = False + running_recording_instance = not args_cli.xr + # Callback closures for the teleop device def reset_recording_instance(): nonlocal should_reset_recording_instance should_reset_recording_instance = True - - # create controller - if args_cli.teleop_device.lower() == "keyboard": - teleop_interface = Se3Keyboard(pos_sensitivity=0.2, rot_sensitivity=0.5) - elif args_cli.teleop_device.lower() == "spacemouse": - teleop_interface = Se3SpaceMouse(pos_sensitivity=0.2, rot_sensitivity=0.5) - elif args_cli.teleop_device.lower() == "handtracking": - from isaacsim.xr.openxr import OpenXRSpec - - teleop_interface = Se3HandTracking(OpenXRSpec.XrHandEXT.XR_HAND_RIGHT_EXT, False, True) - teleop_interface.add_callback("RESET", reset_recording_instance) - viewer = ViewerCfg(eye=(-0.25, -0.3, 0.5), lookat=(0.6, 0, 0), asset_name="viewer") - ViewportCameraController(env, viewer) - else: - raise ValueError( - f"Invalid device interface '{args_cli.teleop_device}'. Supported: 'keyboard', 'spacemouse', 'handtracking'." - ) - + print("Recording instance reset requested") + + def start_recording_instance(): + nonlocal running_recording_instance + running_recording_instance = True + print("Recording started") + + def stop_recording_instance(): + nonlocal running_recording_instance + running_recording_instance = False + print("Recording paused") + + # Set up teleoperation callbacks + teleoperation_callbacks = { + "R": reset_recording_instance, + "START": start_recording_instance, + "STOP": stop_recording_instance, + "RESET": reset_recording_instance, + } + + teleop_interface = setup_teleop_device(teleoperation_callbacks) teleop_interface.add_callback("R", reset_recording_instance) - print(teleop_interface) - # reset before starting + # Reset before starting + env.sim.reset() env.reset() teleop_interface.reset() - # simulate environment -- run everything in inference mode - current_recorded_demo_count = 0 - success_step_count = 0 - with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): - while True: - # get keyboard command - delta_pose, gripper_command = teleop_interface.advance() - # convert to torch - delta_pose = torch.tensor(delta_pose, dtype=torch.float, device=env.device).repeat(env.num_envs, 1) - # compute actions based on environment - actions = pre_process_actions(delta_pose, gripper_command) - - # perform action on environment - env.step(actions) - - if success_term is not None: - if bool(success_term.func(env, **success_term.params)[0]): - success_step_count += 1 - if success_step_count >= args_cli.num_success_steps: - env.recorder_manager.record_pre_reset([0], force_export_or_skip=False) - env.recorder_manager.set_success_to_episodes( - [0], torch.tensor([[True]], dtype=torch.bool, device=env.device) - ) - env.recorder_manager.export_episodes([0]) - should_reset_recording_instance = True - else: - success_step_count = 0 + label_text = f"Recorded {current_recorded_demo_count} successful demonstrations." + instruction_display = setup_ui(label_text, env) - if should_reset_recording_instance: - env.recorder_manager.reset() - env.reset() - should_reset_recording_instance = False - success_step_count = 0 + subtasks = {} - # print out the current demo count if it has changed + with contextlib.suppress(KeyboardInterrupt) and torch.inference_mode(): + while simulation_app.is_running(): + # Get keyboard command + action = teleop_interface.advance() + # Expand to batch dimension + actions = action.repeat(env.num_envs, 1) + + # Perform action on environment + if running_recording_instance: + # Compute actions based on environment + obv = env.step(actions) + if subtasks is not None: + if subtasks == {}: + subtasks = obv[0].get("subtask_terms") + elif subtasks: + show_subtask_instructions(instruction_display, subtasks, obv, env.cfg) + else: + env.sim.render() + + # Check for success condition + success_step_count, success_reset_needed = process_success_condition(env, success_term, success_step_count) + if success_reset_needed: + should_reset_recording_instance = True + + # Update demo count if it has changed if env.recorder_manager.exported_successful_episode_count > current_recorded_demo_count: current_recorded_demo_count = env.recorder_manager.exported_successful_episode_count - print(f"Recorded {current_recorded_demo_count} successful demonstrations.") + label_text = f"Recorded {current_recorded_demo_count} successful demonstrations." + print(label_text) + # Check if we've reached the desired number of demos if args_cli.num_demos > 0 and env.recorder_manager.exported_successful_episode_count >= args_cli.num_demos: - print(f"All {args_cli.num_demos} demonstrations recorded. Exiting the app.") + label_text = f"All {current_recorded_demo_count} demonstrations recorded.\nExiting the app." + instruction_display.show_demo(label_text) + print(label_text) + target_time = time.time() + 0.8 + while time.time() < target_time: + if rate_limiter: + rate_limiter.sleep(env) + else: + env.sim.render() break - # check that simulation is stopped or not + # Handle reset if requested + if should_reset_recording_instance: + success_step_count = handle_reset(env, success_step_count, instruction_display, label_text) + should_reset_recording_instance = False + + # Check if simulation is stopped if env.sim.is_stopped(): break + # Rate limiting if rate_limiter: rate_limiter.sleep(env) + return current_recorded_demo_count + + +def main() -> None: + """Collect demonstrations from the environment using teleop interfaces. + + Main function that orchestrates the entire process: + 1. Sets up rate limiting based on configuration + 2. Creates output directories for saving demonstrations + 3. Configures the environment + 4. Runs the simulation loop to collect demonstrations + 5. Cleans up resources when done + + Raises: + Exception: Propagates exceptions from any of the called functions + """ + # if handtracking is selected, rate limiting is achieved via OpenXR + if args_cli.xr: + rate_limiter = None + from isaaclab.ui.xr_widgets import TeleopVisualizationManager, XRVisualization + + # Assign the teleop visualization manager to the visualization system + XRVisualization.assign_manager(TeleopVisualizationManager) + else: + rate_limiter = RateLimiter(args_cli.step_hz) + + # Set up output directories + output_dir, output_file_name = setup_output_directories() + + # Create and configure environment + global env_cfg # Make env_cfg available to setup_teleop_device + env_cfg, success_term = create_environment_config(output_dir, output_file_name) + + # Create environment + env = create_environment(env_cfg) + + # Run simulation loop + current_recorded_demo_count = run_simulation_loop(env, None, success_term, rate_limiter) + + # Clean up env.close() + print(f"Recording session completed with {current_recorded_demo_count} successful demonstrations") + print(f"Demonstrations saved to: {args_cli.dataset_file}") if __name__ == "__main__": diff --git a/scripts/tools/replay_demos.py b/scripts/tools/replay_demos.py index bd4eca1..e3d776f 100644 --- a/scripts/tools/replay_demos.py +++ b/scripts/tools/replay_demos.py @@ -1,17 +1,12 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - """Script to replay demonstrations with Isaac Lab environments.""" """Launch Isaac Sim Simulator first.""" + import argparse from isaaclab.app import AppLauncher @@ -37,6 +32,12 @@ " --num_envs is 1." ), ) +parser.add_argument( + "--enable_pinocchio", + action="store_true", + default=False, + help="Enable Pinocchio.", +) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) @@ -44,6 +45,11 @@ args_cli = parser.parse_args() # args_cli.headless = True +if args_cli.enable_pinocchio: + # Import pinocchio before AppLauncher to force the use of the version installed by IsaacLab and not the one installed by Isaac Sim + # pinocchio is required by the Pink IK controllers and the GR1T2 retargeter + import pinocchio # noqa: F401 + # launch the simulator app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -55,10 +61,15 @@ import os import torch -from isaaclab.devices import Se3Keyboard +from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler +if args_cli.enable_pinocchio: + import isaaclab_tasks.manager_based.manipulation.pick_place # noqa: F401 + import isaaclab_tasks.manager_based.locomanipulation.pick_place # noqa: F401 + import isaaclab_tasks # noqa: F401 +import uwlab_tasks # noqa: F401 from isaaclab_tasks.utils.parse_cfg import parse_env_cfg is_paused = False @@ -125,7 +136,7 @@ def main(): episode_indices_to_replay = list(range(episode_count)) if args_cli.task is not None: - env_name = args_cli.task + env_name = args_cli.task.split(":")[-1] if env_name is None: raise ValueError("Task/env name was not specified nor found in the dataset.") @@ -138,9 +149,9 @@ def main(): env_cfg.terminations = {} # create environment from loaded config - env = gym.make(env_name, cfg=env_cfg).unwrapped + env = gym.make(args_cli.task, cfg=env_cfg).unwrapped - teleop_interface = Se3Keyboard(pos_sensitivity=0.1, rot_sensitivity=0.1) + teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1)) teleop_interface.add_callback("N", play_cb) teleop_interface.add_callback("B", pause_cb) print('Press "B" to pause and "N" to resume the replayed actions.') @@ -152,6 +163,12 @@ def main(): elif args_cli.validate_states and num_envs > 1: print("Warning: State validation is only supported with a single environment. Skipping state validation.") + # Get idle action (idle actions are applied to envs without next action) + if hasattr(env_cfg, "idle_action"): + idle_action = env_cfg.idle_action.repeat(num_envs, 1) + else: + idle_action = torch.zeros(env.action_space.shape) + # reset before starting env.reset() teleop_interface.reset() @@ -165,8 +182,8 @@ def main(): first_loop = True has_next_action = True while has_next_action: - # initialize actions with zeros so those without next action will not move - actions = torch.zeros(env.action_space.shape) + # initialize actions with idle action so those without next action will not move + actions = idle_action has_next_action = False for env_id in range(num_envs): env_next_action = env_episode_data_map[env_id].get_next_action() diff --git a/scripts/tools/test/test_cosmos_prompt_gen.py b/scripts/tools/test/test_cosmos_prompt_gen.py new file mode 100644 index 0000000..d644d33 --- /dev/null +++ b/scripts/tools/test/test_cosmos_prompt_gen.py @@ -0,0 +1,169 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Test cases for Cosmos prompt generation script.""" + +import json +import os +import tempfile + +import pytest + +from scripts.tools.cosmos.cosmos_prompt_gen import generate_prompt, main + + +@pytest.fixture(scope="class") +def temp_templates_file(): + """Create temporary templates file.""" + temp_file = tempfile.NamedTemporaryFile(suffix=".json", delete=False) + + # Create test templates + test_templates = { + "lighting": ["with bright lighting", "with dim lighting", "with natural lighting"], + "color": ["in warm colors", "in cool colors", "in vibrant colors"], + "style": ["in a realistic style", "in an artistic style", "in a minimalist style"], + "empty_section": [], # Test empty section + "invalid_section": "not a list", # Test invalid section + } + + # Write templates to file + with open(temp_file.name, "w") as f: + json.dump(test_templates, f) + + yield temp_file.name + # Cleanup + os.remove(temp_file.name) + + +@pytest.fixture +def temp_output_file(): + """Create temporary output file.""" + temp_file = tempfile.NamedTemporaryFile(suffix=".txt", delete=False) + yield temp_file.name + # Cleanup + os.remove(temp_file.name) + + +class TestCosmosPromptGen: + """Test cases for Cosmos prompt generation functionality.""" + + def test_generate_prompt_valid_templates(self, temp_templates_file): + """Test generating a prompt with valid templates.""" + prompt = generate_prompt(temp_templates_file) + + # Check that prompt is a string + assert isinstance(prompt, str) + + # Check that prompt contains at least one word + assert len(prompt.split()) > 0 + + # Check that prompt contains valid sections + valid_sections = ["lighting", "color", "style"] + found_sections = [section for section in valid_sections if section in prompt.lower()] + assert len(found_sections) > 0 + + def test_generate_prompt_invalid_file(self): + """Test generating a prompt with invalid file path.""" + with pytest.raises(FileNotFoundError): + generate_prompt("nonexistent_file.json") + + def test_generate_prompt_invalid_json(self): + """Test generating a prompt with invalid JSON file.""" + # Create a temporary file with invalid JSON + with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file: + temp_file.write(b"invalid json content") + temp_file.flush() + + try: + with pytest.raises(ValueError): + generate_prompt(temp_file.name) + finally: + os.remove(temp_file.name) + + def test_main_function_single_prompt(self, temp_templates_file, temp_output_file): + """Test main function with single prompt generation.""" + # Mock command line arguments + import sys + + original_argv = sys.argv + sys.argv = [ + "cosmos_prompt_gen.py", + "--templates_path", + temp_templates_file, + "--num_prompts", + "1", + "--output_path", + temp_output_file, + ] + + try: + main() + + # Check if output file was created + assert os.path.exists(temp_output_file) + + # Check content of output file + with open(temp_output_file) as f: + content = f.read().strip() + assert len(content) > 0 + assert len(content.split("\n")) == 1 + finally: + # Restore original argv + sys.argv = original_argv + + def test_main_function_multiple_prompts(self, temp_templates_file, temp_output_file): + """Test main function with multiple prompt generation.""" + # Mock command line arguments + import sys + + original_argv = sys.argv + sys.argv = [ + "cosmos_prompt_gen.py", + "--templates_path", + temp_templates_file, + "--num_prompts", + "3", + "--output_path", + temp_output_file, + ] + + try: + main() + + # Check if output file was created + assert os.path.exists(temp_output_file) + + # Check content of output file + with open(temp_output_file) as f: + content = f.read().strip() + assert len(content) > 0 + assert len(content.split("\n")) == 3 + + # Check that each line is a valid prompt + for line in content.split("\n"): + assert len(line) > 0 + finally: + # Restore original argv + sys.argv = original_argv + + def test_main_function_default_output(self, temp_templates_file): + """Test main function with default output path.""" + # Mock command line arguments + import sys + + original_argv = sys.argv + sys.argv = ["cosmos_prompt_gen.py", "--templates_path", temp_templates_file, "--num_prompts", "1"] + + try: + main() + + # Check if default output file was created + assert os.path.exists("prompts.txt") + + # Clean up default output file + os.remove("prompts.txt") + finally: + # Restore original argv + sys.argv = original_argv diff --git a/scripts/tools/test/test_hdf5_to_mp4.py b/scripts/tools/test/test_hdf5_to_mp4.py new file mode 100644 index 0000000..ef8f97f --- /dev/null +++ b/scripts/tools/test/test_hdf5_to_mp4.py @@ -0,0 +1,173 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Test cases for HDF5 to MP4 conversion script.""" + +import h5py +import numpy as np +import os +import tempfile + +import pytest + +from scripts.tools.hdf5_to_mp4 import get_num_demos, main, write_demo_to_mp4 + + +@pytest.fixture(scope="class") +def temp_hdf5_file(): + """Create temporary HDF5 file with test data.""" + temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) + with h5py.File(temp_file.name, "w") as h5f: + # Create test data structure + for demo_id in range(2): # Create 2 demos + demo_group = h5f.create_group(f"data/demo_{demo_id}/obs") + + # Create RGB frames (2 frames per demo) + rgb_data = np.random.randint(0, 255, (2, 704, 1280, 3), dtype=np.uint8) + demo_group.create_dataset("table_cam", data=rgb_data) + + # Create segmentation frames + seg_data = np.random.randint(0, 255, (2, 704, 1280, 4), dtype=np.uint8) + demo_group.create_dataset("table_cam_segmentation", data=seg_data) + + # Create normal maps + normals_data = np.random.rand(2, 704, 1280, 3).astype(np.float32) + demo_group.create_dataset("table_cam_normals", data=normals_data) + + # Create depth maps + depth_data = np.random.rand(2, 704, 1280, 1).astype(np.float32) + demo_group.create_dataset("table_cam_depth", data=depth_data) + + yield temp_file.name + # Cleanup + os.remove(temp_file.name) + + +@pytest.fixture +def temp_output_dir(): + """Create temporary output directory.""" + temp_dir = tempfile.mkdtemp() + yield temp_dir + # Cleanup + for file in os.listdir(temp_dir): + os.remove(os.path.join(temp_dir, file)) + os.rmdir(temp_dir) + + +class TestHDF5ToMP4: + """Test cases for HDF5 to MP4 conversion functionality.""" + + def test_get_num_demos(self, temp_hdf5_file): + """Test the get_num_demos function.""" + num_demos = get_num_demos(temp_hdf5_file) + assert num_demos == 2 + + def test_write_demo_to_mp4_rgb(self, temp_hdf5_file, temp_output_dir): + """Test writing RGB frames to MP4.""" + write_demo_to_mp4(temp_hdf5_file, 0, "data/demo_0/obs", "table_cam", temp_output_dir, 704, 1280) + + output_file = os.path.join(temp_output_dir, "demo_0_table_cam.mp4") + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 + + def test_write_demo_to_mp4_segmentation(self, temp_hdf5_file, temp_output_dir): + """Test writing segmentation frames to MP4.""" + write_demo_to_mp4(temp_hdf5_file, 0, "data/demo_0/obs", "table_cam_segmentation", temp_output_dir, 704, 1280) + + output_file = os.path.join(temp_output_dir, "demo_0_table_cam_segmentation.mp4") + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 + + def test_write_demo_to_mp4_normals(self, temp_hdf5_file, temp_output_dir): + """Test writing normal maps to MP4.""" + write_demo_to_mp4(temp_hdf5_file, 0, "data/demo_0/obs", "table_cam_normals", temp_output_dir, 704, 1280) + + output_file = os.path.join(temp_output_dir, "demo_0_table_cam_normals.mp4") + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 + + def test_write_demo_to_mp4_shaded_segmentation(self, temp_hdf5_file, temp_output_dir): + """Test writing shaded_segmentation frames to MP4.""" + write_demo_to_mp4( + temp_hdf5_file, + 0, + "data/demo_0/obs", + "table_cam_shaded_segmentation", + temp_output_dir, + 704, + 1280, + ) + + output_file = os.path.join(temp_output_dir, "demo_0_table_cam_shaded_segmentation.mp4") + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 + + def test_write_demo_to_mp4_depth(self, temp_hdf5_file, temp_output_dir): + """Test writing depth maps to MP4.""" + write_demo_to_mp4(temp_hdf5_file, 0, "data/demo_0/obs", "table_cam_depth", temp_output_dir, 704, 1280) + + output_file = os.path.join(temp_output_dir, "demo_0_table_cam_depth.mp4") + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 + + def test_write_demo_to_mp4_invalid_demo(self, temp_hdf5_file, temp_output_dir): + """Test writing with invalid demo ID.""" + with pytest.raises(KeyError): + write_demo_to_mp4( + temp_hdf5_file, + 999, # Invalid demo ID + "data/demo_999/obs", + "table_cam", + temp_output_dir, + 704, + 1280, + ) + + def test_write_demo_to_mp4_invalid_key(self, temp_hdf5_file, temp_output_dir): + """Test writing with invalid input key.""" + with pytest.raises(KeyError): + write_demo_to_mp4(temp_hdf5_file, 0, "data/demo_0/obs", "invalid_key", temp_output_dir, 704, 1280) + + def test_main_function(self, temp_hdf5_file, temp_output_dir): + """Test the main function.""" + # Mock command line arguments + import sys + + original_argv = sys.argv + sys.argv = [ + "hdf5_to_mp4.py", + "--input_file", + temp_hdf5_file, + "--output_dir", + temp_output_dir, + "--input_keys", + "table_cam", + "table_cam_segmentation", + "--video_height", + "704", + "--video_width", + "1280", + "--framerate", + "30", + ] + + try: + main() + + # Check if output files were created + expected_files = [ + "demo_0_table_cam.mp4", + "demo_0_table_cam_segmentation.mp4", + "demo_1_table_cam.mp4", + "demo_1_table_cam_segmentation.mp4", + ] + + for file in expected_files: + output_file = os.path.join(temp_output_dir, file) + assert os.path.exists(output_file) + assert os.path.getsize(output_file) > 0 + finally: + # Restore original argv + sys.argv = original_argv diff --git a/scripts/tools/test/test_mp4_to_hdf5.py b/scripts/tools/test/test_mp4_to_hdf5.py new file mode 100644 index 0000000..f26fb11 --- /dev/null +++ b/scripts/tools/test/test_mp4_to_hdf5.py @@ -0,0 +1,181 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Test cases for MP4 to HDF5 conversion script.""" + +import h5py +import numpy as np +import os +import tempfile + +import cv2 +import pytest + +from scripts.tools.mp4_to_hdf5 import get_frames_from_mp4, main, process_video_and_demo + + +@pytest.fixture(scope="class") +def temp_hdf5_file(): + """Create temporary HDF5 file with test data.""" + temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) + with h5py.File(temp_file.name, "w") as h5f: + # Create test data structure for 2 demos + for demo_id in range(2): + demo_group = h5f.create_group(f"data/demo_{demo_id}") + obs_group = demo_group.create_group("obs") + + # Create actions data + actions_data = np.random.rand(10, 7).astype(np.float32) + demo_group.create_dataset("actions", data=actions_data) + + # Create robot state data + eef_pos_data = np.random.rand(10, 3).astype(np.float32) + eef_quat_data = np.random.rand(10, 4).astype(np.float32) + gripper_pos_data = np.random.rand(10, 1).astype(np.float32) + obs_group.create_dataset("eef_pos", data=eef_pos_data) + obs_group.create_dataset("eef_quat", data=eef_quat_data) + obs_group.create_dataset("gripper_pos", data=gripper_pos_data) + + # Create camera data + table_cam_data = np.random.randint(0, 255, (10, 704, 1280, 3), dtype=np.uint8) + wrist_cam_data = np.random.randint(0, 255, (10, 704, 1280, 3), dtype=np.uint8) + obs_group.create_dataset("table_cam", data=table_cam_data) + obs_group.create_dataset("wrist_cam", data=wrist_cam_data) + + # Set attributes + demo_group.attrs["num_samples"] = 10 + + yield temp_file.name + # Cleanup + os.remove(temp_file.name) + + +@pytest.fixture(scope="class") +def temp_videos_dir(): + """Create temporary MP4 files.""" + temp_dir = tempfile.mkdtemp() + video_paths = [] + + for demo_id in range(2): + video_path = os.path.join(temp_dir, f"demo_{demo_id}_table_cam.mp4") + video_paths.append(video_path) + + # Create a test video + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + video = cv2.VideoWriter(video_path, fourcc, 30, (1280, 704)) + + # Write some random frames + for _ in range(10): + frame = np.random.randint(0, 255, (704, 1280, 3), dtype=np.uint8) + video.write(frame) + video.release() + + yield temp_dir, video_paths + + # Cleanup + for video_path in video_paths: + os.remove(video_path) + os.rmdir(temp_dir) + + +@pytest.fixture +def temp_output_file(): + """Create temporary output file.""" + temp_file = tempfile.NamedTemporaryFile(suffix=".h5", delete=False) + yield temp_file.name + # Cleanup + os.remove(temp_file.name) + + +class TestMP4ToHDF5: + """Test cases for MP4 to HDF5 conversion functionality.""" + + def test_get_frames_from_mp4(self, temp_videos_dir): + """Test extracting frames from MP4 video.""" + _, video_paths = temp_videos_dir + frames = get_frames_from_mp4(video_paths[0]) + + # Check frame properties + assert frames.shape[0] == 10 # Number of frames + assert frames.shape[1:] == (704, 1280, 3) # Frame dimensions + assert frames.dtype == np.uint8 # Data type + + def test_get_frames_from_mp4_resize(self, temp_videos_dir): + """Test extracting frames with resizing.""" + _, video_paths = temp_videos_dir + target_height, target_width = 352, 640 + frames = get_frames_from_mp4(video_paths[0], target_height, target_width) + + # Check resized frame properties + assert frames.shape[0] == 10 # Number of frames + assert frames.shape[1:] == (target_height, target_width, 3) # Resized dimensions + assert frames.dtype == np.uint8 # Data type + + def test_process_video_and_demo(self, temp_hdf5_file, temp_videos_dir, temp_output_file): + """Test processing a single video and creating a new demo.""" + _, video_paths = temp_videos_dir + with h5py.File(temp_hdf5_file, "r") as f_in, h5py.File(temp_output_file, "w") as f_out: + process_video_and_demo(f_in, f_out, video_paths[0], 0, 2) + + # Check if new demo was created with correct data + assert "data/demo_2" in f_out + assert "data/demo_2/actions" in f_out + assert "data/demo_2/obs/eef_pos" in f_out + assert "data/demo_2/obs/eef_quat" in f_out + assert "data/demo_2/obs/gripper_pos" in f_out + assert "data/demo_2/obs/table_cam" in f_out + assert "data/demo_2/obs/wrist_cam" in f_out + + # Check data shapes + assert f_out["data/demo_2/actions"].shape == (10, 7) + assert f_out["data/demo_2/obs/eef_pos"].shape == (10, 3) + assert f_out["data/demo_2/obs/eef_quat"].shape == (10, 4) + assert f_out["data/demo_2/obs/gripper_pos"].shape == (10, 1) + assert f_out["data/demo_2/obs/table_cam"].shape == (10, 704, 1280, 3) + assert f_out["data/demo_2/obs/wrist_cam"].shape == (10, 704, 1280, 3) + + # Check attributes + assert f_out["data/demo_2"].attrs["num_samples"] == 10 + + def test_main_function(self, temp_hdf5_file, temp_videos_dir, temp_output_file): + """Test the main function.""" + # Mock command line arguments + import sys + + original_argv = sys.argv + sys.argv = [ + "mp4_to_hdf5.py", + "--input_file", + temp_hdf5_file, + "--videos_dir", + temp_videos_dir[0], + "--output_file", + temp_output_file, + ] + + try: + main() + + # Check if output file was created with correct data + with h5py.File(temp_output_file, "r") as f: + # Check if original demos were copied + assert "data/demo_0" in f + assert "data/demo_1" in f + + # Check if new demos were created + assert "data/demo_2" in f + assert "data/demo_3" in f + + # Check data in new demos + for demo_id in [2, 3]: + assert f"data/demo_{demo_id}/actions" in f + assert f"data/demo_{demo_id}/obs/eef_pos" in f + assert f"data/demo_{demo_id}/obs/eef_quat" in f + assert f"data/demo_{demo_id}/obs/gripper_pos" in f + assert f"data/demo_{demo_id}/obs/table_cam" in f + assert f"data/demo_{demo_id}/obs/wrist_cam" in f + finally: + # Restore original argv + sys.argv = original_argv diff --git a/scripts_v2/tools/conversions/convert_mesh.py b/scripts_v2/tools/conversions/convert_mesh.py new file mode 100644 index 0000000..0282188 --- /dev/null +++ b/scripts_v2/tools/conversions/convert_mesh.py @@ -0,0 +1,44 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.app import AppLauncher + +app_launcher = AppLauncher(headless=True) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +from isaaclab.utils.dict import print_dict + +from uwlab_assets.props.workbench.workbench_conversion_cfg import WORKBENCH_CONVERSION_CFG + +from uwlab.sim.converters.mesh_converter import MeshConverter + + +def main(): + for mesh_converter_cfg in WORKBENCH_CONVERSION_CFG: + # Print info + print("-" * 80) + print("-" * 80) + print(f"Input Mesh file: {mesh_converter_cfg.asset_path}") + print("Mesh importer config:") + print_dict(mesh_converter_cfg.to_dict(), nesting=0) # type: ignore + print("-" * 80) + print("-" * 80) + + # Create Mesh converter and import the file + mesh_converter = MeshConverter(mesh_converter_cfg) + # print output + print("Mesh importer output:") + print(f"Generated USD file: {mesh_converter.usd_path}") + print("-" * 80) + print("-" * 80) + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/scripts_v2/tools/record_grasps.py b/scripts_v2/tools/record_grasps.py new file mode 100644 index 0000000..95251a7 --- /dev/null +++ b/scripts_v2/tools/record_grasps.py @@ -0,0 +1,154 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to run grasp sampling using IsaacLab framework.""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +import argparse +import os +import time +import yaml +from tqdm import tqdm +from typing import cast + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Grasp sampling for end effector on objects.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") +parser.add_argument("--task", type=str, default="OmniReset-Robotiq2f85-GraspSampling-v0", help="Name of the task.") +parser.add_argument("--dataset_dir", type=str, default="./grasp_datasets/", help="Directory to save grasp results.") +parser.add_argument("--num_grasps", type=int, default=500, help="Number of grasp candidates to evaluate.") + +AppLauncher.add_app_launcher_args(parser) +args_cli, remaining_args = parser.parse_known_args() + +# Launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything else.""" + +import gymnasium as gym +import torch + +import isaaclab_tasks # noqa: F401 +from isaaclab.envs import ManagerBasedRLEnv +from isaaclab.managers.recorder_manager import DatasetExportMode + +from uwlab.utils.datasets.torch_dataset_file_handler import TorchDatasetFileHandler + +import uwlab_tasks # noqa: F401 +import uwlab_tasks.manager_based.manipulation.reset_states.mdp as task_mdp +from uwlab_tasks.utils.hydra import hydra_task_compose + +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True +torch.backends.cudnn.deterministic = False +torch.backends.cudnn.benchmark = False + + +@hydra_task_compose(args_cli.task, "env_cfg_entry_point", hydra_args=remaining_args) +def main(env_cfg, agent_cfg) -> None: + """Main function to run grasp sampling.""" + # create directory if it does not exist + if not os.path.exists(args_cli.dataset_dir): + os.makedirs(args_cli.dataset_dir, exist_ok=True) + + # Get USD path for hash computation + object_usd_path = env_cfg.scene.object.spawn.usd_path + + # Compute hash for this object + dataset_hash = task_mdp.utils.compute_assembly_hash(object_usd_path) + + # Update info.yaml with this hash and USD path + info_file = os.path.join(args_cli.dataset_dir, "info.yaml") + info_data = {} + if os.path.exists(info_file): + with open(info_file) as f: + info_data = yaml.safe_load(f) or {} + + info_data[dataset_hash] = {"object_usd_path": object_usd_path} + + with open(info_file, "w") as f: + yaml.dump(info_data, f, default_flow_style=False) + + print(f"Recording grasps for hash: {dataset_hash}") + print(f"Object: {object_usd_path}") + + # Configure recorder for hash-based saving + env_cfg.recorders = task_mdp.GraspRelativePoseRecorderManagerCfg( + robot_name="robot", + object_name="object", + gripper_body_name="robotiq_base_link", + ) + env_cfg.recorders.dataset_export_dir_path = args_cli.dataset_dir + env_cfg.recorders.dataset_filename = f"{dataset_hash}.pt" + env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_SUCCEEDED_ONLY + env_cfg.recorders.dataset_file_handler_class_type = TorchDatasetFileHandler + + # 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 + + # make sure environment is non-deterministic, so we don't get redundant trajectories between datasets! + env_cfg.seed = None + + # Create environment + env = cast(ManagerBasedRLEnv, gym.make(args_cli.task, cfg=env_cfg)).unwrapped + + # Reset environment (this will trigger grasp sampling event) + env.reset() + + # Run grasp sampling + num_grasps_evaluated = 0 + current_successful_grasps = 0 + + # Create progress bar for successful grasps + pbar = tqdm(total=args_cli.num_grasps, desc="Successful grasps", unit="grasps") + actions = -torch.ones(env.action_space.shape, device=env.device, dtype=torch.float32) + + start_time = time.time() + + while current_successful_grasps < args_cli.num_grasps: + # Step environment (this will evaluate grasps in parallel across environments) + _, _, terminated, truncated, _ = env.step(actions) + dones = terminated | truncated + + # Update progress based on successful grasps + new_successful_count = env.recorder_manager.exported_successful_episode_count + if new_successful_count > current_successful_grasps: + increment = new_successful_count - current_successful_grasps + current_successful_grasps = new_successful_count + pbar.update(increment) + + # Count total grasps evaluated (sum across all environments) + num_grasps_evaluated += dones.sum().item() + + # Check if simulation should stop + if env.sim.is_stopped(): + break + + pbar.close() + + # Get final statistics + final_successful_grasps = env.recorder_manager.exported_successful_episode_count + + print("Grasp sampling complete!") + print(f"Total grasps evaluated: {num_grasps_evaluated}") + print(f"Successful grasps: {final_successful_grasps}") + if num_grasps_evaluated > 0: + print(f"Success rate: {final_successful_grasps / num_grasps_evaluated:.2%}") + print(f"Time taken: {(time.time() - start_time) / 60:.2f} minutes") + + env.close() + + +if __name__ == "__main__": + main() + simulation_app.close() diff --git a/scripts_v2/tools/record_partial_assemblies.py b/scripts_v2/tools/record_partial_assemblies.py new file mode 100644 index 0000000..c25f061 --- /dev/null +++ b/scripts_v2/tools/record_partial_assemblies.py @@ -0,0 +1,223 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to record partial assemblies using IsaacLab framework.""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +import argparse +import os +import torch +import yaml +from tqdm import tqdm +from typing import cast + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Record partial assemblies for object pairs.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") +parser.add_argument("--task", type=str, default="UW-FBLeg-PartialAssemblies-v0", help="Name of the task.") +parser.add_argument( + "--dataset_dir", type=str, default="./partial_assembly_datasets/", help="Directory to save assembly results." +) +parser.add_argument( + "--num_trajectories", type=int, default=1, help="Number of physics trajectories to run for pose discovery." +) +parser.add_argument("--pos_similarity_threshold", type=float, default=0.001, help="Threshold for pose similarity.") +parser.add_argument( + "--ori_similarity_threshold", type=float, default=0.01, help="Threshold for orientation similarity." +) + +AppLauncher.add_app_launcher_args(parser) +args_cli, remaining_args = parser.parse_known_args() + +# Launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything else.""" + +import gymnasium as gym +import time + +import isaaclab_tasks # noqa: F401 +from isaaclab.envs import ManagerBasedRLEnv + +import uwlab_tasks # noqa: F401 +from uwlab_tasks.manager_based.manipulation.reset_states.mdp.utils import compute_assembly_hash +from uwlab_tasks.utils.hydra import hydra_task_compose + +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True +torch.backends.cudnn.deterministic = False +torch.backends.cudnn.benchmark = False + + +@hydra_task_compose(args_cli.task, "env_cfg_entry_point", hydra_args=remaining_args) +def main(env_cfg, agent_cfg) -> None: + """Main function to record partial assemblies.""" + # create directory if it does not exist + if not os.path.exists(args_cli.dataset_dir): + os.makedirs(args_cli.dataset_dir, exist_ok=True) + + # 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 + + # make sure environment is non-deterministic for diverse pose discovery + env_cfg.seed = None + + # Create environment + env = cast(ManagerBasedRLEnv, gym.make(args_cli.task, cfg=env_cfg)).unwrapped + + # Get USD paths for hash computation + insertive_usd_path = env_cfg.scene.insertive_object.spawn.usd_path + receptive_usd_path = env_cfg.scene.receptive_object.spawn.usd_path + + # Compute hash for this object combination + dataset_hash = compute_assembly_hash(insertive_usd_path, receptive_usd_path) + + # Update info.yaml with this hash and USD paths + info_file = os.path.join(args_cli.dataset_dir, "info.yaml") + info_data = {} + if os.path.exists(info_file): + with open(info_file) as f: + info_data = yaml.safe_load(f) or {} + + info_data[dataset_hash] = { + "insertive_object_usd_path": insertive_usd_path, + "receptive_object_usd_path": receptive_usd_path, + } + + with open(info_file, "w") as f: + yaml.dump(info_data, f, default_flow_style=False) + + print(f"Recording partial assemblies for hash: {dataset_hash}") + print(f"Insertive: {insertive_usd_path}") + print(f"Receptive: {receptive_usd_path}") + + # Reset environment (this will position objects at assembled state) + env.reset() + + # Initialize pose tracking + recorded_poses = [] + all_recorded_poses = None # Keep track of ALL recorded poses for uniqueness checking + + # Run pose discovery + episode_count = 0 + actions = torch.zeros(env.action_space.shape, device=env.device, dtype=torch.float32) + + # Create progress bar + pbar = tqdm(total=args_cli.num_trajectories, desc="Trajectories", unit="episodes") + start_time = time.time() + total_poses_collected = 0 + + while episode_count < args_cli.num_trajectories: + # Step environment (forces will be applied automatically by pose_discovery event) + _, rewards, terminated, truncated, _ = env.step(actions) + dones = terminated | truncated + + # Reset environments that are done and update progress + if dones.any(): + episodes_completed = dones.sum().item() + episode_count += episodes_completed + pbar.update(episodes_completed) + + # Get all pose data and use rewards from step + if "current_pose_data" in env.extras["log"]: + valid_mask = rewards > 0 + + if valid_mask.any(): + # Filter pose data to only valid environments + all_poses_data = env.extras["log"]["current_pose_data"] + valid_poses_data = {key: all_poses_data[key][valid_mask] for key in all_poses_data.keys()} + + # Calculate relative poses for similarity checking + relative_poses = valid_poses_data["relative_pose"] + + # Check uniqueness against ALL previously recorded poses + if all_recorded_poses is not None: + # Calculate distance matrices between all pairs + relative_pos = relative_poses[:, :3] # (N, 3) + relative_quat = relative_poses[:, 3:] # (N, 4) + all_recorded_pos = all_recorded_poses[:, :3] # (M, 3) + all_recorded_quat = all_recorded_poses[:, 3:] # (M, 4) + + # Compute distance matrices: (N, M) + pos_dists = torch.cdist(relative_pos, all_recorded_pos, p=2) # Euclidean distance + ori_dists = torch.cdist(relative_quat, all_recorded_quat, p=2) # Euclidean distance + + # Find minimum distance to any previously recorded pose + min_pos_dists = torch.min(pos_dists, dim=1)[0] # (N,) + min_ori_dists = torch.min(ori_dists, dim=1)[0] # (N,) + new_pose_mask = (min_pos_dists > args_cli.pos_similarity_threshold) & ( + min_ori_dists > args_cli.ori_similarity_threshold + ) + else: + new_pose_mask = torch.ones(len(relative_poses), dtype=torch.bool, device=env.device) + + # Save new unique poses + if new_pose_mask.any(): + new_poses = {key: valid_poses_data[key][new_pose_mask] for key in valid_poses_data.keys()} + recorded_poses.append(new_poses) + + # Update all recorded poses for comparison + if all_recorded_poses is None: + all_recorded_poses = relative_poses[new_pose_mask] + else: + all_recorded_poses = torch.cat([all_recorded_poses, relative_poses[new_pose_mask]], dim=0) + + # Update total poses collected + new_count = sum(len(batch["relative_position"]) for batch in recorded_poses) + if new_count > total_poses_collected: + total_poses_collected = new_count + + else: + # No valid poses this step, continue + pass + + # Check if simulation should stop + if env.sim.is_stopped(): + break + + # Save any remaining poses + if recorded_poses: + _save_poses_to_dataset(recorded_poses, args_cli.dataset_dir, dataset_hash) + + pbar.close() + + print("Partial assembly recording complete!") + print(f"Trajectories completed: {episode_count}") + print(f"Poses recorded: {total_poses_collected}") + print(f"Time taken: {(time.time() - start_time) / 60:.2f} minutes") + if episode_count > 0: + print(f"Average poses per trajectory: {total_poses_collected / episode_count:.1f}") + + env.close() + + +def _save_poses_to_dataset(pose_batches: list, dataset_dir: str, dataset_hash: str) -> None: + """Save pose batches to Torch dataset (.pt).""" + if not pose_batches: + return + + # Concatenate all batches into single arrays + all_poses = {} + for key in pose_batches[0].keys(): + all_poses[key] = torch.cat([batch[key] for batch in pose_batches], dim=0).cpu() + + # Save as Torch .pt file + output_file = os.path.join(dataset_dir, f"{dataset_hash}.pt") + torch.save(all_poses, output_file) + + print(f"Saved {len(all_poses['relative_position'])} poses to {output_file}") + + +if __name__ == "__main__": + main() + simulation_app.close() diff --git a/scripts_v2/tools/record_reset_states.py b/scripts_v2/tools/record_reset_states.py new file mode 100644 index 0000000..6ba6346 --- /dev/null +++ b/scripts_v2/tools/record_reset_states.py @@ -0,0 +1,173 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to record reset states using IsaacLab framework.""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + +import argparse +import os +import torch +import yaml +from tqdm import tqdm +from typing import cast + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Record reset states for object pairs.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") +parser.add_argument( + "--task", type=str, default="OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0", help="Name of the task." +) +parser.add_argument( + "--dataset_dir", type=str, default="./reset_state_datasets/", help="Directory to save reset state results." +) +parser.add_argument( + "--num_reset_states", type=int, default=100, help="Number of reset states to record. Set to 0 for infinite." +) + +AppLauncher.add_app_launcher_args(parser) +args_cli, remaining_args = parser.parse_known_args() + +# Launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything else.""" + +import gymnasium as gym +import time + +import isaaclab_tasks # noqa: F401 +from isaaclab.envs import ManagerBasedRLEnv +from isaaclab.managers.recorder_manager import DatasetExportMode + +from uwlab.utils.datasets.torch_dataset_file_handler import TorchDatasetFileHandler + +import uwlab_tasks # noqa: F401 +import uwlab_tasks.manager_based.manipulation.reset_states.mdp as task_mdp +from uwlab_tasks.utils.hydra import hydra_task_compose + +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True +torch.backends.cudnn.deterministic = False +torch.backends.cudnn.benchmark = False + + +@hydra_task_compose(args_cli.task, "env_cfg_entry_point", hydra_args=remaining_args) +def main(env_cfg, agent_cfg) -> None: + """Main function to record reset states.""" + # create directory if it does not exist + if not os.path.exists(args_cli.dataset_dir): + os.makedirs(args_cli.dataset_dir, exist_ok=True) + + # 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 + + # make sure environment is non-deterministic for diverse pose discovery + env_cfg.seed = None + + # Get USD paths for hash computation + insertive_usd_path = env_cfg.scene.insertive_object.spawn.usd_path + receptive_usd_path = env_cfg.scene.receptive_object.spawn.usd_path + reset_state_hash = task_mdp.utils.compute_assembly_hash(insertive_usd_path, receptive_usd_path) + + # Update info.yaml with this hash and USD paths + info_file = os.path.join(args_cli.dataset_dir, "info.yaml") + info_data = {} + if os.path.exists(info_file): + with open(info_file) as f: + info_data = yaml.safe_load(f) or {} + + info_data[reset_state_hash] = { + "insertive_object_usd_path": insertive_usd_path, + "receptive_object_usd_path": receptive_usd_path, + } + + with open(info_file, "w") as f: + yaml.dump(info_data, f, default_flow_style=False) + + print(f"Recording reset states for hash: {reset_state_hash}") + print(f"Insertive: {insertive_usd_path}") + print(f"Receptive: {receptive_usd_path}") + + # Setup recording configuration + output_dir = args_cli.dataset_dir + output_file_name = f"{reset_state_hash}.pt" + + env_cfg.recorders = task_mdp.StableStateRecorderManagerCfg() + env_cfg.recorders.dataset_export_dir_path = output_dir + env_cfg.recorders.dataset_filename = output_file_name + env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_SUCCEEDED_ONLY + env_cfg.recorders.dataset_file_handler_class_type = TorchDatasetFileHandler + + # create environment + env = cast(ManagerBasedRLEnv, gym.make(args_cli.task, cfg=env_cfg)).unwrapped + env.reset() + + # Run reset state sampling + num_reset_conditions_evaluated = 0 + current_successful_reset_conditions = 0 + actions = torch.zeros(env.action_space.shape, device=env.device, dtype=torch.float32) + if "ObjectAnywhereEEGrasped" in args_cli.task or "ObjectRestingEEGrasped" in args_cli.task: + actions[:, -1] = -1.0 + else: + actions[:, -1] = ( + torch.randint(0, 2, (env.num_envs,), device=env.device, dtype=torch.float32) * 2 - 1 + ) # Randomly choose between -1 and 1 + + # Create progress bar + pbar = tqdm(total=args_cli.num_reset_states, desc="Successful reset states", unit="reset states") + + start_time = time.time() + + while current_successful_reset_conditions < args_cli.num_reset_states: + # Step environment (this will evaluate grasps in parallel across environments) + _, _, terminated, truncated, _ = env.step(actions) + dones = terminated | truncated + done_idx = torch.where(dones)[0] + + # Reset actions for environments that are done + if done_idx.numel() > 0 and not ( + "ObjectAnywhereEEGrasped" in args_cli.task or "ObjectRestingEEGrasped" in args_cli.task + ): + actions[done_idx, -1] = ( + torch.randint(0, 2, (done_idx.numel(),), device=env.device, dtype=torch.float32) * 2 - 1 + ) + + # Update progress based on successful reset conditions + new_successful_count = env.recorder_manager.exported_successful_episode_count + if new_successful_count > current_successful_reset_conditions: + increment = new_successful_count - current_successful_reset_conditions + current_successful_reset_conditions = new_successful_count + pbar.update(increment) + + # Count total reset conditions evaluated (sum across all environments) + num_reset_conditions_evaluated += dones.sum().item() + + if env.sim.is_stopped(): + break + + pbar.close() + + # Get final statistics + final_successful_reset_conditions = env.recorder_manager.exported_successful_episode_count + print("Reset state recording complete!") + print(f"Total reset conditions evaluated: {num_reset_conditions_evaluated}") + print(f"Successful reset conditions: {final_successful_reset_conditions}") + if num_reset_conditions_evaluated > 0: + print(f"Success rate: {final_successful_reset_conditions / num_reset_conditions_evaluated:.2%}") + print(f"Time taken: {(time.time() - start_time) / 60:.2f} minutes") + + env.close() + + +if __name__ == "__main__": + main() + simulation_app.close() diff --git a/scripts_v2/tools/visualize_reset_states.py b/scripts_v2/tools/visualize_reset_states.py new file mode 100644 index 0000000..21009b5 --- /dev/null +++ b/scripts_v2/tools/visualize_reset_states.py @@ -0,0 +1,116 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Script to visualize saved states from HDF5 dataset.""" + +from __future__ import annotations + +import argparse +import time +import torch +from typing import cast + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Visualize saved reset states from a dataset directory.") +parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to simulate.") +parser.add_argument("--task", type=str, default=None, help="Name of the task.") +parser.add_argument( + "--dataset_dir", + type=str, + default="./reset_state_datasets", + help="Directory containing reset-state datasets saved as .pt", +) +parser.add_argument("--reset_interval", type=float, default=0.1, help="Time interval between resets in seconds.") + +AppLauncher.add_app_launcher_args(parser) +args_cli, remaining_args = parser.parse_known_args() + +# launch omniverse app +app_launcher = AppLauncher(headless=args_cli.headless) +simulation_app = app_launcher.app + +"""Rest everything else.""" + +import contextlib +import gymnasium as gym + +from isaaclab.envs import ManagerBasedRLEnv +from isaaclab.managers import EventTermCfg as EventTerm + +from uwlab_tasks.manager_based.manipulation.reset_states.mdp import events as task_mdp +from uwlab_tasks.utils.hydra import hydra_task_compose + +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True +torch.backends.cudnn.deterministic = False +torch.backends.cudnn.benchmark = False + + +@hydra_task_compose(args_cli.task, "env_cfg_entry_point", hydra_args=remaining_args) +def main(env_cfg, agent_cfg) -> None: + # 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 + + # make sure environment is non-deterministic for diverse pose discovery + env_cfg.seed = None + + # Set up the MultiResetManager to load states from the computed dataset + reset_from_reset_states = EventTerm( + func=task_mdp.MultiResetManager, + mode="reset", + params={ + "base_paths": [args_cli.dataset_dir], + "probs": [1.0], + "success": "env.reward_manager.get_term_cfg('progress_context').func.success", + }, + ) + + # Add the reset manager to the environment configuration + env_cfg.events.reset_from_reset_states = reset_from_reset_states + + # create environment + env = cast(ManagerBasedRLEnv, gym.make(args_cli.task, cfg=env_cfg)).unwrapped + env.reset() + + # Initialize variables + print(f"Starting visualization of saved states from {args_cli.dataset_dir}") + print("Press Ctrl+C to stop") + + with contextlib.suppress(KeyboardInterrupt): + while True: + asset = env.unwrapped.scene["robot"] + # specific for robotiq + gripper_joint_positions = asset.data.joint_pos[:, asset.find_joints(["right_inner_finger_joint"])[0][0]] + gripper_closed_fraction = ( + torch.abs(gripper_joint_positions) / env_cfg.actions.gripper.close_command_expr["finger_joint"] + ) + gripper_mask = gripper_closed_fraction > 0.1 + # Step the simulation + for _ in range(5): + action = torch.zeros(env.action_space.shape, device=env.device, dtype=torch.float32) + action[gripper_mask, -1] = -1.0 + action[~gripper_mask, -1] = 1.0 + env.step(action) + for _ in range(5): + env.unwrapped.sim.step() + success = env.unwrapped.reward_manager.get_term_cfg("progress_context").func.success + print("Success: ", success) + + # Wait for the specified interval + time.sleep(args_cli.reset_interval) + + # Reset the environment to load a new state + env.reset() + + env.close() + + +if __name__ == "__main__": + main() + # close sim app + simulation_app.close() diff --git a/source/uwlab/config/extension.toml b/source/uwlab/config/extension.toml index bce5a49..8d0e771 100644 --- a/source/uwlab/config/extension.toml +++ b/source/uwlab/config/extension.toml @@ -1,13 +1,13 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "0.8.5" +version = "0.8.6" # Description title = "UW Lab framework for Robot Learning" description="Extension providing main framework interfaces and abstractions for robot learning." readme = "docs/README.md" -repository = "https://github.com/UW-Lab/UWLab" +repository = "https://github.com/uw-lab/UWLab" category = "robotics" keywords = ["kit", "robotics", "learning", "ai"] @@ -25,7 +25,6 @@ requirements = [ "prettytable==3.3.0", "toml", "hidapi", - "gymnasium==0.29.0", "trimesh" ] diff --git a/source/uwlab/docs/CHANGELOG.rst b/source/uwlab/docs/CHANGELOG.rst index a24f1da..0e2e824 100644 --- a/source/uwlab/docs/CHANGELOG.rst +++ b/source/uwlab/docs/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog --------- +0.8.6 (2025-10-09) +~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Removed data_manager and uwlab version of interactive scene so to track IsaacLab implementation + + 0.8.5 (2025-03-23) ~~~~~~~~~~~~~~~~~~ @@ -65,7 +74,7 @@ Changed Removed ^^^^^^^ -* Deprecating :folder:`uwlab.envs.assets.deformable` related deformable modules +* Deprecating :mod:`uwlab.envs.assets.deformable` related deformable modules 0.7.1 (2024-10-24) @@ -94,7 +103,7 @@ Fixed ^^^^^ * Dropping DeformableInteractiveScene from :file:`uwlab.scene.deformable_interactive_scene_cfg` as -official deformable has been added to Isaac Lab + official deformable has been added to Isaac Lab 0.6.0 (2024-10-20) @@ -121,7 +130,7 @@ Added Added ^^^^^ -* Transferred Experimental Evolution code into lab extension as :dir:`uwlab.evolution_system` +* Transferred Experimental Evolution code into lab extension as :mod:`uwlab.evolution_system` 0.5.3 (2024-09-02) ~~~~~~~~~~~~~~~~~~ @@ -130,7 +139,7 @@ Added ^^^^^ * Adding event that reset from demonstration :func:`uwlab.envs.mdp.events.reset_from_demonstration` -* Adding event that record state of simulation:func:`uwlab.envs.mdp.events.record_state_configuration` +* Adding event that record state of simulation :func:`uwlab.envs.mdp.events.record_state_configuration` 0.5.2 (2024-09-01) ~~~~~~~~~~~~~~~~~~ @@ -157,7 +166,7 @@ Added ^^^^^ * Added features that support obj typed sub-terrain, and custom supply of the spawning locations - please check :folder:`uwlab.lab.terrains` + please check :mod:`uwlab.lab.terrains` 0.4.3 (2024-08-06) @@ -202,8 +211,8 @@ Added Added ^^^^^^^ -Added experiment feature categorical command type for commanding anything that can be represented -by integer at :folder:`uwlab.envs.mdp.commands` +* Added experiment feature categorical command type for commanding anything that can be represented + by integer at :mod:`uwlab.envs.mdp.commands` 0.2.7 (2024-07-28) @@ -357,7 +366,7 @@ Changed * :class:`uwlab.actuators.actuator_pd.py.HebiStrategy3Actuator` reflected the field that scales position_p and effort_p * :class:`uwlab.actuators.actuator_pd.py.HebiStrategy4Actuator` reflected the field that scales position_p and effort_p * Improved Reuseability :class:`uwlab.devices.rokoko_udp_receiver.Rokoko_Glove` such that the returned joint position respects the -order user inputs. Added debug visualization. Plan to add scale by knuckle width to match the leap hand knuckle width + order user inputs. Added debug visualization. Plan to add scale by knuckle width to match the leap hand knuckle width 0.1.5 (2024-07-04) ~~~~~~~~~~~~~~~~~~ @@ -366,8 +375,8 @@ order user inputs. Added debug visualization. Plan to add scale by knuckle width Changed ^^^^^^^ * :meth:`uwlab.envs.data_manager_based_rl.step` the actual environment update rate now becomes -decimation square, as square allows a nice property that tuning decimation creates minimal effect on the learning -behavior. + decimation square, as square allows a nice property that tuning decimation creates minimal effect on the learning + behavior. 0.1.4 (2024-06-29) diff --git a/source/uwlab/setup.py b/source/uwlab/setup.py index d7cea5f..64fec27 100644 --- a/source/uwlab/setup.py +++ b/source/uwlab/setup.py @@ -1,3 +1,8 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + """Installation script for the 'uwlab' python package.""" import os diff --git a/source/uwlab/uwlab/actuators/__init__.py b/source/uwlab/uwlab/actuators/__init__.py index fece1e8..0ad7f84 100644 --- a/source/uwlab/uwlab/actuators/__init__.py +++ b/source/uwlab/uwlab/actuators/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/actuators/actuator_cfg.py b/source/uwlab/uwlab/actuators/actuator_cfg.py index 498a4d3..8a136ed 100644 --- a/source/uwlab/uwlab/actuators/actuator_cfg.py +++ b/source/uwlab/uwlab/actuators/actuator_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/actuators/actuator_pd.py b/source/uwlab/uwlab/actuators/actuator_pd.py index 50fdebd..cedb9f1 100644 --- a/source/uwlab/uwlab/actuators/actuator_pd.py +++ b/source/uwlab/uwlab/actuators/actuator_pd.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/__init__.py b/source/uwlab/uwlab/assets/__init__.py index bed6d2d..2b8c3c9 100644 --- a/source/uwlab/uwlab/assets/__init__.py +++ b/source/uwlab/uwlab/assets/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/articulation/__init__.py b/source/uwlab/uwlab/assets/articulation/__init__.py index 0128118..b3df420 100644 --- a/source/uwlab/uwlab/assets/articulation/__init__.py +++ b/source/uwlab/uwlab/assets/articulation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/articulation/articulation.py b/source/uwlab/uwlab/assets/articulation/articulation.py index b86d032..7247e3d 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation.py +++ b/source/uwlab/uwlab/assets/articulation/articulation.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -18,14 +13,13 @@ from prettytable import PrettyTable from typing import TYPE_CHECKING -import isaacsim.core.utils.stage as stage_utils -import omni.log -from pxr import PhysxSchema, UsdPhysics - import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils +import isaacsim.core.utils.stage as stage_utils +import omni.log from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator from isaaclab.utils.types import ArticulationActions +from pxr import PhysxSchema, UsdPhysics from ..asset_base import AssetBase from .articulation_data import ArticulationData @@ -1489,19 +1483,17 @@ def _log_articulation_joint_info(self): table.align["Name"] = "l" # add info on each term for index, name in enumerate(self.joint_names): - table.add_row( - [ - index, - name, - stiffnesses[index], - dampings[index], - armatures[index], - frictions[index], - position_limits[index], - velocity_limits[index], - effort_limits[index], - ] - ) + table.add_row([ + index, + name, + stiffnesses[index], + dampings[index], + armatures[index], + frictions[index], + position_limits[index], + velocity_limits[index], + effort_limits[index], + ]) # read out all tendon parameters from simulation if self.num_fixed_tendons > 0: @@ -1527,14 +1519,12 @@ def _log_articulation_joint_info(self): ] # 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], + ]) diff --git a/source/uwlab/uwlab/assets/articulation/articulation_cfg.py b/source/uwlab/uwlab/assets/articulation/articulation_cfg.py index cf4a57b..c16fde8 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_cfg.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/articulation/articulation_data.py b/source/uwlab/uwlab/assets/articulation/articulation_data.py index b393742..817962e 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_data.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_data.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,9 +6,8 @@ import torch import weakref -import omni.log - import isaaclab.utils.math as math_utils +import omni.log from isaaclab.utils.buffers import TimestampedBuffer from .articulation_view import ArticulationView @@ -437,7 +431,7 @@ def body_acc_w(self): @property def projected_gravity_b(self): """Projection of the gravity direction on base frame. Shape is (num_instances, 3).""" - return math_utils.quat_rotate_inverse(self.root_link_quat_w, self.GRAVITY_VEC_W) + return math_utils.quat_apply_inverse(self.root_link_quat_w, self.GRAVITY_VEC_W) @property def heading_w(self): @@ -532,7 +526,7 @@ def root_lin_vel_b(self) -> torch.Tensor: This quantity is the linear velocity of the articulation root's center of mass frame relative to the world with respect to the articulation root's actor frame. """ - return math_utils.quat_rotate_inverse(self.root_quat_w, self.root_lin_vel_w) + return math_utils.quat_apply_inverse(self.root_quat_w, self.root_lin_vel_w) @property def root_ang_vel_b(self) -> torch.Tensor: @@ -541,7 +535,7 @@ def root_ang_vel_b(self) -> torch.Tensor: This quantity is the angular velocity of the articulation root's center of mass frame relative to the world with respect to the articulation root's actor frame. """ - return math_utils.quat_rotate_inverse(self.root_quat_w, self.root_ang_vel_w) + return math_utils.quat_apply_inverse(self.root_quat_w, self.root_ang_vel_w) # # Derived Root Link Frame Properties @@ -604,7 +598,7 @@ def root_link_lin_vel_b(self) -> torch.Tensor: This quantity is the linear velocity of the actor frame of the root rigid body frame with respect to the rigid body's actor frame. """ - return math_utils.quat_rotate_inverse(self.root_link_quat_w, self.root_link_lin_vel_w) + return math_utils.quat_apply_inverse(self.root_link_quat_w, self.root_link_lin_vel_w) @property def root_link_ang_vel_b(self) -> torch.Tensor: @@ -613,7 +607,7 @@ def root_link_ang_vel_b(self) -> torch.Tensor: This quantity is the angular velocity of the actor frame of the root rigid body frame with respect to the rigid body's actor frame. """ - return math_utils.quat_rotate_inverse(self.root_link_quat_w, self.root_link_ang_vel_w) + return math_utils.quat_apply_inverse(self.root_link_quat_w, self.root_link_ang_vel_w) # # Root Center of Mass state properties @@ -679,7 +673,7 @@ def root_com_lin_vel_b(self) -> torch.Tensor: This quantity is the linear velocity of the root rigid body's center of mass frame with respect to the rigid body's actor frame. """ - return math_utils.quat_rotate_inverse(self.root_link_quat_w, self.root_com_lin_vel_w) + return math_utils.quat_apply_inverse(self.root_link_quat_w, self.root_com_lin_vel_w) @property def root_com_ang_vel_b(self) -> torch.Tensor: @@ -688,7 +682,7 @@ def root_com_ang_vel_b(self) -> torch.Tensor: This quantity is the angular velocity of the root rigid body's center of mass frame with respect to the rigid body's actor frame. """ - return math_utils.quat_rotate_inverse(self.root_link_quat_w, self.root_com_ang_vel_w) + return math_utils.quat_apply_inverse(self.root_link_quat_w, self.root_com_ang_vel_w) @property def body_vel_w(self) -> torch.Tensor: diff --git a/source/uwlab/uwlab/assets/articulation/articulation_drive/__init__.py b/source/uwlab/uwlab/assets/articulation/articulation_drive/__init__.py index 44e81d7..8fac0b4 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_drive/__init__.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_drive/__init__.py @@ -1,3 +1,8 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + from .articulation_drive import ArticulationDrive from .articulation_drive_cfg import ArticulationDriveCfg from .articulation_drive_data import ArticulationDriveData diff --git a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive.py b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive.py index ec5f3f4..d757b17 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_cfg.py b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_cfg.py index a9523ae..2e4d83d 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_cfg.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_cfg.py @@ -1,10 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable from isaaclab.utils import configclass diff --git a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_data.py b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_data.py index 58a1aa8..37f9968 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_data.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_data.py @@ -1,18 +1,18 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import torch -from typing import List, TypedDict +from typing import TypedDict class ArticulationDriveData(TypedDict): is_running: bool close: bool - link_names: List[str] - dof_names: List[str] - dof_types: List[str] + link_names: list[str] + dof_names: list[str] + dof_types: list[str] pos: torch.Tensor vel: torch.Tensor torque: torch.Tensor diff --git a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_process.py b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_process.py index c9ea25f..2168612 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_process.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_drive/articulation_drive_process.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/articulation/articulation_view/__init__.py b/source/uwlab/uwlab/assets/articulation/articulation_view/__init__.py index 69aa3a8..7b3319a 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_view/__init__.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_view/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view.py b/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view.py index 02057b4..bcec0ee 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view.py @@ -1,19 +1,19 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import torch from abc import abstractmethod -from typing import List, TypedDict +from typing import TypedDict class SharedDataSchema(TypedDict): is_running: bool close: bool - link_names: List[str] - dof_names: List[str] - dof_types: List[str] + link_names: list[str] + dof_names: list[str] + dof_types: list[str] pos: torch.Tensor vel: torch.Tensor torque: torch.Tensor diff --git a/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view_cfg.py b/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view_cfg.py index 20addbf..44d2124 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view_cfg.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_view/articulation_view_cfg.py @@ -1,12 +1,13 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from collections.abc import Callable from dataclasses import MISSING -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING from isaaclab.utils import configclass diff --git a/source/uwlab/uwlab/assets/articulation/articulation_view/bullet_articulation_view.py b/source/uwlab/uwlab/assets/articulation/articulation_view/bullet_articulation_view.py index 1c6f0d2..b070d2b 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_view/bullet_articulation_view.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_view/bullet_articulation_view.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/assets/articulation/articulation_view/utils/articulation_kinematics.py b/source/uwlab/uwlab/assets/articulation/articulation_view/utils/articulation_kinematics.py index a7bbfa7..22191ce 100644 --- a/source/uwlab/uwlab/assets/articulation/articulation_view/utils/articulation_kinematics.py +++ b/source/uwlab/uwlab/assets/articulation/articulation_view/utils/articulation_kinematics.py @@ -1,13 +1,11 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import torch -from typing import List, Union import pybullet as p - from isaaclab.utils import math as math_utils @@ -141,7 +139,7 @@ def get_root_link_velocity(self, clone: bool = True) -> torch.Tensor: def get_link_transforms( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.link_transforms[:, body_indices] @@ -149,7 +147,7 @@ def get_link_transforms( def get_link_velocities( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.link_velocities[:, body_indices] @@ -157,7 +155,7 @@ def get_link_velocities( def get_link_coms( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: """in body local frames""" @@ -166,7 +164,7 @@ def get_link_coms( def get_link_masses( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.link_mass[:, body_indices] @@ -174,7 +172,7 @@ def get_link_masses( def get_link_inertias( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.link_inertia[:, body_indices] @@ -182,7 +180,7 @@ def get_link_inertias( def get_dof_limits( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_limits[:, :, body_indices] @@ -190,7 +188,7 @@ def get_dof_limits( def get_dof_positions( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_positions[:, body_indices] @@ -198,7 +196,7 @@ def get_dof_positions( def get_dof_position_targets( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_position_target[:, body_indices] @@ -206,7 +204,7 @@ def get_dof_position_targets( def get_dof_velocities( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_velocities[:, body_indices] @@ -214,7 +212,7 @@ def get_dof_velocities( def get_dof_velocity_targets( self, - body_indices: Union[List[int], torch.Tensor, slice] = slice(None), + body_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_velocity_target[:, body_indices] @@ -222,7 +220,7 @@ def get_dof_velocity_targets( def get_dof_max_velocities( self, - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_max_velocity[:, joint_indices] @@ -230,7 +228,7 @@ def get_dof_max_velocities( def get_dof_torques( self, - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_torques[:, joint_indices] @@ -238,7 +236,7 @@ def get_dof_torques( def get_dof_max_forces( self, - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_max_forces[:, joint_indices] @@ -246,7 +244,7 @@ def get_dof_max_forces( def get_dof_stiffnesses( self, - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ): data = self.articulation_view_data.dof_stiffness[:, joint_indices] @@ -254,7 +252,7 @@ def get_dof_stiffnesses( def get_dof_dampings( self, - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ) -> torch.Tensor: data = self.articulation_view_data.dof_damping[:, joint_indices] @@ -262,7 +260,7 @@ def get_dof_dampings( def get_dof_frictions( self, - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ): data = self.articulation_view_data.dof_frictions[:, joint_indices] @@ -270,7 +268,7 @@ def get_dof_frictions( def get_dof_armatures( self, - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), clone: bool = True, ): data = self.articulation_view_data.dof_armatures[:, joint_indices] @@ -327,8 +325,8 @@ def get_jacobian(self) -> torch.Tensor: def set_masses( self, masses: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - link_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + link_indices: list[int] | torch.Tensor | slice = slice(None), ): """ Set the mass of each link in PyBullet at runtime using p.changeDynamics. @@ -356,23 +354,23 @@ def set_masses( def set_root_velocities( self, root_velocities: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), ): pass def set_dof_limits( self, limits: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_limits[indices, joint_indices] = limits.to(self.device) def set_dof_positions( self, positions: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): with torch.inference_mode(mode=True): # Also store internally @@ -398,16 +396,16 @@ def set_dof_positions( def set_dof_position_targets( self, positions: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_position_target[indices, joint_indices] = positions.to(self.device) def set_dof_velocities( self, velocities: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_velocities[indices, joint_indices] = velocities.to(self.device) @@ -432,16 +430,16 @@ def set_dof_velocities( def set_dof_velocity_targets( self, velocities: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_velocity_target[indices, joint_indices] = velocities.to(self.device) def set_dof_torques( self, torques: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_torques[indices, joint_indices] = torques.to(self.device) @@ -450,8 +448,8 @@ def set_dof_states( positions: torch.Tensor, velocities: torch.Tensor, efforts: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.set_dof_positions(positions, indices, joint_indices) self.set_dof_velocities(velocities, indices, joint_indices) @@ -460,32 +458,32 @@ def set_dof_states( def set_dof_stiffnesses( self, stiffness: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_stiffness[indices, joint_indices] = stiffness.to(self.device) def set_dof_dampings( self, damping: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_damping[indices, joint_indices] = damping.to(self.device) def set_dof_armatures( self, armatures: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_armatures[indices, joint_indices] = armatures.to(self.device) def set_dof_frictions( self, frictions: torch.Tensor, - indices: Union[List[int], torch.Tensor, slice] = slice(None), - joint_indices: Union[List[int], torch.Tensor, slice] = slice(None), + indices: list[int] | torch.Tensor | slice = slice(None), + joint_indices: list[int] | torch.Tensor | slice = slice(None), ): self.articulation_view_data.dof_frictions[indices, joint_indices] = frictions.to(self.device) diff --git a/source/uwlab/uwlab/assets/asset_base.py b/source/uwlab/uwlab/assets/asset_base.py index 3e2b720..34ce212 100644 --- a/source/uwlab/uwlab/assets/asset_base.py +++ b/source/uwlab/uwlab/assets/asset_base.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -16,11 +11,10 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any +import isaaclab.sim as sim_utils import omni.kit.app import omni.timeline -import isaaclab.sim as sim_utils - if TYPE_CHECKING: from .asset_base_cfg import AssetBaseCfg diff --git a/source/uwlab/uwlab/assets/asset_base_cfg.py b/source/uwlab/uwlab/assets/asset_base_cfg.py index 479cc04..e17ad5c 100644 --- a/source/uwlab/uwlab/assets/asset_base_cfg.py +++ b/source/uwlab/uwlab/assets/asset_base_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/controllers/__init__.py b/source/uwlab/uwlab/controllers/__init__.py index ff4bb75..7e7e5c8 100644 --- a/source/uwlab/uwlab/controllers/__init__.py +++ b/source/uwlab/uwlab/controllers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/controllers/differential_ik.py b/source/uwlab/uwlab/controllers/differential_ik.py index 22f9365..6830c8a 100644 --- a/source/uwlab/uwlab/controllers/differential_ik.py +++ b/source/uwlab/uwlab/controllers/differential_ik.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/controllers/differential_ik_cfg.py b/source/uwlab/uwlab/controllers/differential_ik_cfg.py index dfef637..0c2033a 100644 --- a/source/uwlab/uwlab/controllers/differential_ik_cfg.py +++ b/source/uwlab/uwlab/controllers/differential_ik_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/devices/__init__.py b/source/uwlab/uwlab/devices/__init__.py index a477fb3..196a631 100644 --- a/source/uwlab/uwlab/devices/__init__.py +++ b/source/uwlab/uwlab/devices/__init__.py @@ -1,5 +1,5 @@ -# Copyright (c) 2022-2025, The UW Lab Project Developers. -# All rights reserved. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/devices/device_cfg.py b/source/uwlab/uwlab/devices/device_cfg.py index 7080734..4302f67 100644 --- a/source/uwlab/uwlab/devices/device_cfg.py +++ b/source/uwlab/uwlab/devices/device_cfg.py @@ -1,9 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from typing import Callable, Literal +from collections.abc import Callable +from typing import Literal from isaaclab.devices import DeviceBase from isaaclab.utils import configclass diff --git a/source/uwlab/uwlab/devices/realsense_t265.py b/source/uwlab/uwlab/devices/realsense_t265.py index 895e5db..c8e5453 100644 --- a/source/uwlab/uwlab/devices/realsense_t265.py +++ b/source/uwlab/uwlab/devices/realsense_t265.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,6 +11,7 @@ from isaaclab.devices import DeviceBase from isaaclab.utils.math import compute_pose_error + from uwlab.utils.math import create_axis_remap_function if TYPE_CHECKING: diff --git a/source/uwlab/uwlab/devices/rokoko_glove.py b/source/uwlab/uwlab/devices/rokoko_glove.py index 2b80612..c2d4a6c 100644 --- a/source/uwlab/uwlab/devices/rokoko_glove.py +++ b/source/uwlab/uwlab/devices/rokoko_glove.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/devices/se3_keyboard.py b/source/uwlab/uwlab/devices/se3_keyboard.py index 5fdedf8..57768aa 100644 --- a/source/uwlab/uwlab/devices/se3_keyboard.py +++ b/source/uwlab/uwlab/devices/se3_keyboard.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/devices/teleop.py b/source/uwlab/uwlab/devices/teleop.py index a7bb49f..31160b5 100644 --- a/source/uwlab/uwlab/devices/teleop.py +++ b/source/uwlab/uwlab/devices/teleop.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,14 +6,16 @@ from __future__ import annotations import torch +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Callable, Literal +from typing import TYPE_CHECKING, Literal import isaaclab.utils.math as math_utils from isaaclab.devices import DeviceBase from isaaclab.managers import SceneEntityCfg from isaaclab.markers import VisualizationMarkers from isaaclab.markers.config import CUBOID_MARKER_CFG, FRAME_MARKER_CFG + from uwlab.utils.math import create_axis_remap_function if TYPE_CHECKING: diff --git a/source/uwlab/uwlab/devices/teleop_cfg.py b/source/uwlab/uwlab/devices/teleop_cfg.py index 2a1db4a..e1bb73f 100644 --- a/source/uwlab/uwlab/devices/teleop_cfg.py +++ b/source/uwlab/uwlab/devices/teleop_cfg.py @@ -1,13 +1,15 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable, Literal +from typing import Literal from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + from uwlab.devices import DeviceBaseTeleopCfg from .teleop import Teleop diff --git a/source/uwlab/uwlab/envs/__init__.py b/source/uwlab/uwlab/envs/__init__.py index ffd07cc..dbfb5e1 100644 --- a/source/uwlab/uwlab/envs/__init__.py +++ b/source/uwlab/uwlab/envs/__init__.py @@ -1,5 +1,5 @@ -# Copyright (c) 2022-2025, The UW Lab Project Developers. -# All rights reserved. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -27,7 +27,5 @@ Based on these workflows, there are the following environment classes for single and multi-agent RL: """ -from .data_manager_based_rl import DataManagerBasedRLEnv -from .data_manager_based_rl_cfg import DataManagerBasedRLEnvCfg from .real_rl_env import RealRLEnv from .real_rl_env_cfg import RealRLEnvCfg diff --git a/source/uwlab/uwlab/envs/common.py b/source/uwlab/uwlab/envs/common.py new file mode 100644 index 0000000..790f70e --- /dev/null +++ b/source/uwlab/uwlab/envs/common.py @@ -0,0 +1,146 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import gymnasium as gym +import torch +from typing import Dict, Literal, TypeVar + +from isaaclab.utils import configclass + +## +# Configuration. +## + + +@configclass +class ViewerCfg: + """Configuration of the scene viewport camera.""" + + eye: tuple[float, float, float] = (7.5, 7.5, 7.5) + """Initial camera position (in m). Default is (7.5, 7.5, 7.5).""" + + lookat: tuple[float, float, float] = (0.0, 0.0, 0.0) + """Initial camera target position (in m). Default is (0.0, 0.0, 0.0).""" + + cam_prim_path: str = "/OmniverseKit_Persp" + """The camera prim path to record images from. Default is "/OmniverseKit_Persp", + which is the default camera in the viewport. + """ + + resolution: tuple[int, int] = (1280, 720) + """The resolution (width, height) of the camera specified using :attr:`cam_prim_path`. + Default is (1280, 720). + """ + + origin_type: Literal["world", "env", "asset_root", "asset_body"] = "world" + """The frame in which the camera position (eye) and target (lookat) are defined in. Default is "world". + + Available options are: + + * ``"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`. + """ + + env_index: int = 0 + """The environment index for frame origin. Default is 0. + + This quantity is only effective if :attr:`origin` is set to "env" or "asset_root". + """ + + asset_name: str | None = None + """The asset name in the interactive scene for the frame origin. Default is None. + + This quantity is only effective if :attr:`origin` is set to "asset_root". + """ + + body_name: str | None = None + """The name of the body in :attr:`asset_name` in the interactive scene for the frame origin. Default is None. + + This quantity is only effective if :attr:`origin` is set to "asset_body". + """ + + +## +# Types. +## + +SpaceType = TypeVar("SpaceType", gym.spaces.Space, int, set, tuple, list, dict) +"""A sentinel object to indicate a valid space type to specify states, observations and actions.""" + +VecEnvObs = Dict[str, torch.Tensor | Dict[str, torch.Tensor]] +"""Observation returned by the environment. + +The observations are stored in a dictionary. The keys are the group to which the observations belong. +This is useful for various setups such as reinforcement learning with asymmetric actor-critic or +multi-agent learning. For non-learning paradigms, this may include observations for different components +of a system. + +Within each group, the observations can be stored either as a dictionary with keys as the names of each +observation term in the group, or a single tensor obtained from concatenating all the observation terms. +For example, for asymmetric actor-critic, the observation for the actor and the critic can be accessed +using the keys ``"policy"`` and ``"critic"`` respectively. + +Note: + By default, most learning frameworks deal with default and privileged observations in different ways. + This handling must be taken care of by the wrapper around the :class:`ManagerBasedRLEnv` instance. + + For included frameworks (RSL-RL, RL-Games, skrl), the observations must have the key "policy". In case, + the key "critic" is also present, then the critic observations are taken from the "critic" group. + Otherwise, they are the same as the "policy" group. + +""" + +VecEnvStepReturn = tuple[VecEnvObs, torch.Tensor, torch.Tensor, torch.Tensor, dict] +"""The environment signals processed at the end of each step. + +The tuple contains batched information for each sub-environment. The information is stored in the following order: + +1. **Observations**: The observations from the environment. +2. **Rewards**: The rewards from the environment. +3. **Terminated Dones**: Whether the environment reached a terminal state, such as task success or robot falling etc. +4. **Timeout Dones**: Whether the environment reached a timeout state, such as end of max episode length. +5. **Extras**: A dictionary containing additional information from the environment. +""" + +AgentID = TypeVar("AgentID") +"""Unique identifier for an agent within a multi-agent environment. + +The identifier has to be an immutable object, typically a string (e.g.: ``"agent_0"``). +""" + +ObsType = TypeVar("ObsType", torch.Tensor, Dict[str, torch.Tensor]) +"""A sentinel object to indicate the data type of the observation. +""" + +ActionType = TypeVar("ActionType", torch.Tensor, Dict[str, torch.Tensor]) +"""A sentinel object to indicate the data type of the action. +""" + +StateType = TypeVar("StateType", torch.Tensor, dict) +"""A sentinel object to indicate the data type of the state. +""" + +EnvStepReturn = tuple[ + Dict[AgentID, ObsType], + Dict[AgentID, torch.Tensor], + Dict[AgentID, torch.Tensor], + Dict[AgentID, torch.Tensor], + Dict[AgentID, dict], +] +"""The environment signals processed at the end of each step. + +The tuple contains batched information for each sub-environment (keyed by the agent ID). +The information is stored in the following order: + +1. **Observations**: The observations from the environment. +2. **Rewards**: The rewards from the environment. +3. **Terminated Dones**: Whether the environment reached a terminal state, such as task success or robot falling etc. +4. **Timeout Dones**: Whether the environment reached a timeout state, such as end of max episode length. +5. **Extras**: A dictionary containing additional information from the environment. +""" diff --git a/source/uwlab/uwlab/envs/data_manager_based_rl.py b/source/uwlab/uwlab/envs/data_manager_based_rl.py deleted file mode 100644 index f472137..0000000 --- a/source/uwlab/uwlab/envs/data_manager_based_rl.py +++ /dev/null @@ -1,317 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import torch -from collections.abc import Sequence - -from isaaclab.envs import ManagerBasedEnvCfg, ManagerBasedRLEnv -from isaaclab.envs.manager_based_env import VecEnvObs -from isaaclab.managers import EventManager -from isaaclab.markers import VisualizationMarkers -from isaaclab.markers.config import FRAME_MARKER_CFG -from isaaclab.utils.timer import Timer -from uwlab.scene import InteractiveScene - -from ..managers.data_manager import DataManager -from .data_manager_based_rl_cfg import DataManagerBasedRLEnvCfg - -VecEnvStepReturn = tuple[VecEnvObs, torch.Tensor, torch.Tensor, torch.Tensor, dict] - - -class DataManagerBasedRLEnv(ManagerBasedRLEnv): - """The superclass for flexible reinforcement learning-based environments. - - This class extending the :class:`ManagerBasedRLEnv` and implements the flexible modules - that makes experimentation and hacking easier while still using manager based style. - Specifically, the two fields, extensions and data manager. - The field self.extension is a dictionary serves as a place to store any data that needs - a centralized access that can not be modularized by managers, this is usually the last - resort when no other optional are easy and it is not recommended to use in any formalized code. - Data Manager in another hand, can be used to provide more structured data structure to be accessed - by rest of managers. In practice, this module should be used carefully and avoid at best - when formalizing the code and only use for advanced special treatment. - - """ - - cfg: DataManagerBasedRLEnvCfg - """Configuration for the environment.""" - - def __init__(self, cfg: DataManagerBasedRLEnvCfg, render_mode: str | None = None, **kwargs): - self.extensions = {} - # there is no good way to override the InteractiveScene class, so we need to patch it - self._interactive_scene_patched_manager_based_env_init(cfg) - self._manager_based_rl_env_init(cfg, render_mode) - # RLTaskEnv.__bases__ = (BaseEnv, gym.Env) - frame_marker_cfg = FRAME_MARKER_CFG.copy() - frame_marker_cfg.markers["frame"].scale = (0.02, 0.02, 0.02) - self.goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal")) - self.symmetry_augmentation_func = None - if hasattr(cfg, "symmetry_augmentation_func"): - self.symmetry_augmentation_func = cfg.symmetry_augmentation_func - - def _interactive_scene_patched_manager_based_env_init(self, cfg: ManagerBasedEnvCfg): - import builtins - - import omni.log - - from isaaclab.envs.ui import ViewportCameraController - from isaaclab.sim import SimulationContext - - # check that the config is valid - cfg.validate() - # store inputs to class - self.cfg = cfg - # initialize internal variables - self._is_closed = False - - # set the seed for the environment - 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.") - - # create a simulation context to control the simulator - if SimulationContext.instance() is None: - # the type-annotation is required to avoid a type-checking error - # since it gets confused with Isaac Sim's SimulationContext class - self.sim: SimulationContext = SimulationContext(self.cfg.sim) - else: - # simulation context should only be created before the environment - # when in extension mode - if not builtins.ISAAC_LAUNCHED_FROM_TERMINAL: - raise RuntimeError("Simulation context already exists. Cannot create a new one.") - self.sim: SimulationContext = SimulationContext.instance() - - # make sure torch is running on the correct device - if "cuda" in self.device: - torch.cuda.set_device(self.device) - - # print useful information - print("[INFO]: Base environment:") - print(f"\tEnvironment device : {self.device}") - print(f"\tEnvironment seed : {self.cfg.seed}") - print(f"\tPhysics step-size : {self.physics_dt}") - print(f"\tRendering step-size : {self.physics_dt * self.cfg.sim.render_interval}") - print(f"\tEnvironment step-size : {self.step_dt}") - - if self.cfg.sim.render_interval < self.cfg.decimation: - msg = ( - f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " - f"({self.cfg.decimation}). Multiple 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) - - # counter for simulation steps - self._sim_step_counter = 0 - - # generate scene - with Timer("[INFO]: Time taken for scene creation", "scene_creation"): - self.scene = InteractiveScene(self.cfg.scene) - print("[INFO]: Scene manager: ", self.scene) - - # set up camera viewport controller - # viewport is not available in other rendering modes so the function will throw a warning - # FIXME: This needs to be fixed in the future when we unify the UI functionalities even for - # non-rendering modes. - if self.sim.render_mode >= self.sim.RenderMode.PARTIAL_RENDERING: - self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer) - else: - self.viewport_camera_controller = None - - # create event manager - # note: this is needed here (rather than after simulation play) to allow USD-related randomization events - # that must happen before the simulation starts. Example: randomizing mesh scale - self.event_manager = EventManager(self.cfg.events, self) - print("[INFO] Event Manager: ", self.event_manager) - - # apply USD-related randomization events - if "prestartup" in self.event_manager.available_modes: - self.event_manager.apply(mode="prestartup") - - # play the simulator to activate physics handles - # note: this activates the physics simulation view that exposes TensorAPIs - # note: when started in extension mode, first call sim.reset_async() and then initialize the managers - if builtins.ISAAC_LAUNCHED_FROM_TERMINAL is False: - print("[INFO]: Starting the simulation. This may take a few seconds. Please wait...") - with Timer("[INFO]: Time taken for simulation start", "simulation_start"): - self.sim.reset() - # add timeline event to load managers - self.load_managers() - - # extend UI elements - # we need to do this here after all the managers are initialized - # this is because they dictate the sensors and commands right now - if self.sim.has_gui() and self.cfg.ui_window_class_type is not None: - # setup live visualizers - self.setup_manager_visualizers() - self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") - else: - # if no window, then we don't need to store the window - self._window = None - - # allocate dictionary to store metrics - self.extras = {} - - # initialize observation buffers - self.obs_buf = {} - - def _manager_based_rl_env_init(self, cfg: DataManagerBasedRLEnvCfg, render_mode): - # store the render mode - self.render_mode = render_mode - - # initialize data and constants - # -- counter for curriculum - self.common_step_counter = 0 - # -- init buffers - self.episode_length_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.long) - # -- 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...") - - @property - def is_closed(self): - return self._is_closed - - """ - Patches - """ - - def patch_interactive_scene(self): - import sys - - # Make sure we have not imported the original module yet - if "isaaclab.scene" in sys.modules: - del sys.modules["isaaclab.scene"] - - import uwlab.scene - - sys.modules["isaaclab.scene"] = uwlab.scene - - def unpatch_interactive_scene(self): - import sys - - if "isaaclab.scene" in sys.modules: - del sys.modules["isaaclab.scene"] - - """ - Operations - MDP - """ - - def step(self, action: torch.Tensor) -> VecEnvStepReturn: - """Execute one time-step of the environment's dynamics and reset terminated environments. - - Unlike the :class:`ManagerBasedEnvCfg.step` class, the function performs the following operations: - - 1. Process the actions. - 2. Perform physics stepping. - 3. Perform rendering if gui is enabled. - 4. Update the environment counters and compute the rewards and terminations. - 5. Reset the environments that terminated. - 6. Compute the observations. - 7. Return the observations, rewards, resets and extras. - - Args: - action: The actions to apply on the environment. Shape is (num_envs, action_dim). - - Returns: - A tuple containing the observations, rewards, resets (terminated and truncated) and extras. - """ - # process actions - self.action_manager.process_action(action) - - self.recorder_manager.record_pre_step() - - # check if we need to do rendering within the physics loop - # note: checked here once to avoid multiple checks within the loop - is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() - - for _ in range(self.cfg.decimation): - self._sim_step_counter += 1 - # set actions into buffers - self.action_manager.apply_action() - # set actions into simulator - self.scene.write_data_to_sim() - # simulate - self.sim.step(render=False) - # render between steps only if the GUI or an RTX sensor needs it - # note: we assume the render interval to be the shortest accepted rendering interval. - # If a camera needs rendering at a faster frequency, this will lead to unexpected behavior. - if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: - self.sim.render() - # update buffers at sim dt - self.scene.update(dt=self.physics_dt) - - if hasattr(self, "data_manager"): - self.data_manager.compute() - # post-step: - # -- update env counters (used for curriculum generation) - self.episode_length_buf += 1 # step in current episode (per env) - self.common_step_counter += 1 # total step (common for all envs) - # -- check terminations - self.reset_buf = self.termination_manager.compute() - self.reset_terminated = self.termination_manager.terminated - self.reset_time_outs = self.termination_manager.time_outs - # -- reward computation - self.reward_buf = self.reward_manager.compute(dt=self.step_dt) - - if len(self.recorder_manager.active_terms) > 0: - # update observations for recording if needed - self.obs_buf = self.observation_manager.compute() - self.recorder_manager.record_post_step() - - # -- reset envs that terminated/timed-out and log the episode information - reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) - if len(reset_env_ids) > 0: - # trigger recorder terms for pre-reset calls - self.recorder_manager.record_pre_reset(reset_env_ids) - - self._reset_idx(reset_env_ids) - # update articulation kinematics - 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() - - # trigger recorder terms for post-reset calls - self.recorder_manager.record_post_reset(reset_env_ids) - - # -- update command - self.command_manager.compute(dt=self.step_dt) - # -- step interval events - if "interval" in self.event_manager.available_modes: - self.event_manager.apply(mode="interval", dt=self.step_dt) - # -- compute observations - # note: done after reset to get the correct observations for reset envs - self.obs_buf = self.observation_manager.compute() - - # return observations, rewards, resets and extras - return self.obs_buf, self.reward_buf, self.reset_terminated, self.reset_time_outs, self.extras - - def load_managers(self): - if getattr(self.cfg, "data", None) is not None: - self.data_manager: DataManager = DataManager(self.cfg.data, self) - print("[INFO] Data Manager: ", self.data_manager) - super().load_managers() - - def _reset_idx(self, env_ids: Sequence[int]): - if getattr(self, "data_manager", None) is not None: - self.data_manager.reset(env_ids) - return super()._reset_idx(env_ids) - - def close(self): - for key, val in self.extensions.items(): - del val - super().close() diff --git a/source/uwlab/uwlab/envs/data_manager_based_rl_cfg.py b/source/uwlab/uwlab/envs/data_manager_based_rl_cfg.py deleted file mode 100644 index 256405d..0000000 --- a/source/uwlab/uwlab/envs/data_manager_based_rl_cfg.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from isaaclab.envs.manager_based_rl_env_cfg import ManagerBasedRLEnvCfg -from isaaclab.utils import configclass - - -@configclass -class DataManagerBasedRLEnvCfg(ManagerBasedRLEnvCfg): - data: object | None = None diff --git a/source/uwlab/uwlab/envs/diagnosis/__init__.py b/source/uwlab/uwlab/envs/diagnosis/__init__.py index e9bbfdb..ca8aa4d 100644 --- a/source/uwlab/uwlab/envs/diagnosis/__init__.py +++ b/source/uwlab/uwlab/envs/diagnosis/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/diagnosis/diagnosis.py b/source/uwlab/uwlab/envs/diagnosis/diagnosis.py index aca32d3..af3823c 100644 --- a/source/uwlab/uwlab/envs/diagnosis/diagnosis.py +++ b/source/uwlab/uwlab/envs/diagnosis/diagnosis.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,17 +6,18 @@ from __future__ import annotations import torch -from typing import TYPE_CHECKING, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING from isaaclab.assets import Articulation from isaaclab.managers import SceneEntityCfg if TYPE_CHECKING: - from uwlab.envs import DataManagerBasedRLEnv + from isaaclab.envs import ManagerBasedRLEnv def get_link_incoming_joint_force( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -37,7 +38,7 @@ def get_link_incoming_joint_force( def get_dof_projected_joint_forces( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -60,7 +61,7 @@ def get_dof_projected_joint_forces( def get_dof_applied_torque( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -76,7 +77,7 @@ def get_dof_applied_torque( def get_dof_computed_torque( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -88,7 +89,7 @@ def get_dof_computed_torque( def get_dof_target_position( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -100,7 +101,7 @@ def get_dof_target_position( def get_dof_position( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -112,7 +113,7 @@ def get_dof_position( def get_dof_target_velocity( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -124,7 +125,7 @@ def get_dof_target_velocity( def get_dof_velocity( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -136,7 +137,7 @@ def get_dof_velocity( def get_dof_acceleration( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -148,14 +149,14 @@ def get_dof_acceleration( def get_action_rate( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, ) -> torch.Tensor: return torch.square(env.action_manager.action[env_ids] - env.action_manager.prev_action[env_ids]) def get_joint_torque_utilization( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -169,7 +170,7 @@ def get_joint_torque_utilization( def get_joint_velocity_utilization( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -183,7 +184,7 @@ def get_joint_velocity_utilization( def get_joint_power( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -197,7 +198,7 @@ def get_joint_power( def get_joint_mechanical_work( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -217,7 +218,7 @@ def get_joint_mechanical_work( def effective_torque( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: @@ -255,7 +256,7 @@ def effective_torque( def get_dof_weight_distribution( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, env_ids: Sequence[int] | torch.Tensor | None, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: diff --git a/source/uwlab/uwlab/envs/diagnosis/diagnosis_util.py b/source/uwlab/uwlab/envs/diagnosis/diagnosis_util.py index e7fcb52..acb8dfb 100644 --- a/source/uwlab/uwlab/envs/diagnosis/diagnosis_util.py +++ b/source/uwlab/uwlab/envs/diagnosis/diagnosis_util.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -19,8 +19,8 @@ def get_dof_stress_von_mises_optimized( c: float, ) -> torch.Tensor: # Transform forces and torques to body frame - forces_b = math_utils.quat_rotate_inverse(bodies_quat, forces_w) - torques_b = math_utils.quat_rotate_inverse(bodies_quat, torques_w) + forces_b = math_utils.quat_apply_inverse(bodies_quat, forces_w) + torques_b = math_utils.quat_apply_inverse(bodies_quat, torques_w) # Extract force and torque components F = forces_b # Shape: (..., 3) diff --git a/source/uwlab/uwlab/envs/mdp/__init__.py b/source/uwlab/uwlab/envs/mdp/__init__.py index 85937c3..47423bf 100644 --- a/source/uwlab/uwlab/envs/mdp/__init__.py +++ b/source/uwlab/uwlab/envs/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/actions/__init__.py b/source/uwlab/uwlab/envs/mdp/actions/__init__.py index a01912b..ffdacb9 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/__init__.py +++ b/source/uwlab/uwlab/envs/mdp/actions/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/actions/actions_cfg.py b/source/uwlab/uwlab/envs/mdp/actions/actions_cfg.py index 7e9374f..4374f1f 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/actions_cfg.py +++ b/source/uwlab/uwlab/envs/mdp/actions/actions_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -12,6 +12,7 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.managers.action_manager import ActionTerm from isaaclab.utils import configclass + from uwlab.envs.mdp.actions import ( default_joint_static_action, pca_actions, diff --git a/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py b/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py index 587877c..75bb4e2 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py +++ b/source/uwlab/uwlab/envs/mdp/actions/default_joint_static_action.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/actions/pca_actions.py b/source/uwlab/uwlab/envs/mdp/actions/pca_actions.py index 8b84fba..b1e9b68 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/pca_actions.py +++ b/source/uwlab/uwlab/envs/mdp/actions/pca_actions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,7 +11,6 @@ from typing import TYPE_CHECKING import requests - from isaaclab.envs.mdp.actions import JointPositionAction if TYPE_CHECKING: diff --git a/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py b/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py index aabb7c6..ca6e985 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py +++ b/source/uwlab/uwlab/envs/mdp/actions/task_space_actions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,9 +9,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log - import isaaclab.utils.math as math_utils +import omni.log from isaaclab.assets.articulation import Articulation from isaaclab.managers.action_manager import ActionTerm diff --git a/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py b/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py index e539036..4869da0 100644 --- a/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py +++ b/source/uwlab/uwlab/envs/mdp/actions/visualizable_joint_target_position.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/commands/__init__.py b/source/uwlab/uwlab/envs/mdp/commands/__init__.py index 1d67466..6eb4c5e 100644 --- a/source/uwlab/uwlab/envs/mdp/commands/__init__.py +++ b/source/uwlab/uwlab/envs/mdp/commands/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py b/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py index 1da43ba..7f78727 100644 --- a/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py +++ b/source/uwlab/uwlab/envs/mdp/commands/categorical_command.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/commands/commands_cfg.py b/source/uwlab/uwlab/envs/mdp/commands/commands_cfg.py index 90f1964..e0e1866 100644 --- a/source/uwlab/uwlab/envs/mdp/commands/commands_cfg.py +++ b/source/uwlab/uwlab/envs/mdp/commands/commands_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/curriculums.py b/source/uwlab/uwlab/envs/mdp/curriculums.py new file mode 100644 index 0000000..06f2496 --- /dev/null +++ b/source/uwlab/uwlab/envs/mdp/curriculums.py @@ -0,0 +1,292 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Common functions that can be used to create curriculum for the learning environment. + +The functions can be passed to the :class:`isaaclab.managers.CurriculumTermCfg` object to enable +the curriculum introduced by the function. +""" + +from __future__ import annotations + +import re +from collections.abc import Sequence +from typing import TYPE_CHECKING, ClassVar + +from isaaclab.managers import CurriculumTermCfg, ManagerTermBase + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +class modify_reward_weight(ManagerTermBase): + """Curriculum that modifies the reward weight based on a step-wise schedule.""" + + def __init__(self, cfg: CurriculumTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + + # obtain term configuration + term_name = cfg.params["term_name"] + self._term_cfg = env.reward_manager.get_term_cfg(term_name) + + def __call__( + self, + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + term_name: str, + weight: float, + num_steps: int, + ) -> float: + # update term settings + if env.common_step_counter > num_steps: + self._term_cfg.weight = weight + env.reward_manager.set_term_cfg(term_name, self._term_cfg) + + return self._term_cfg.weight + + +class modify_env_param(ManagerTermBase): + """Curriculum term for modifying an environment parameter at runtime. + + This term helps modify an environment parameter (or attribute) at runtime. + This parameter can be any attribute of the environment, such as the physics material properties, + observation ranges, or any other configurable parameter that can be accessed via a dotted path. + + The term uses the ``address`` parameter to specify the target attribute as a dotted path string. + For instance, "event_manager.cfg.object_physics_material.func.material_buckets" would + refer to the attribute ``material_buckets`` in the event manager's event term "object_physics_material", + which is a tensor of sampled physics material properties. + + The term uses the ``modify_fn`` parameter to specify the function that modifies the value of the target attribute. + The function should have the signature: + + .. code-block:: python + + def modify_fn(env, env_ids, old_value, **modify_params) -> new_value | modify_env_param.NO_CHANGE: + ... + + 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`` + are additional parameters that can be passed to the function. The function should return + the new value to be set for the target attribute, or the special token ``modify_env_param.NO_CHANGE`` + to indicate that the value should not be changed. + + At the first call to the term after initialization, it compiles getter and setter functions + for the target attribute specified by the ``address`` parameter. The getter retrieves the + 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. + + Usage: + .. code-block:: python + + def resample_bucket_range( + env, env_id, data, static_friction_range, dynamic_friction_range, restitution_range, num_steps + ): + if env.common_step_counter > num_steps: + range_list = [static_friction_range, dynamic_friction_range, restitution_range] + ranges = torch.tensor(range_list, device="cpu") + new_buckets = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(data), 3), device="cpu") + return new_buckets + + # if the step counter is not reached, return NO_CHANGE to indicate no modification. + # we do this instead of returning None, since None is a valid value to set. + # additionally, returning the input data would not change the value but still lead + # 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={ + "address": "event_manager.cfg.object_physics_material.func.material_buckets", + "modify_fn": resample_bucket_range, + "modify_params": { + "static_friction_range": [0.5, 1.0], + "dynamic_friction_range": [0.3, 1.0], + "restitution_range": [0.0, 0.5], + "num_step": 120000 + } + } + ) + """ + + NO_CHANGE: ClassVar = object() + """Special token to indicate no change in the value to be set. + + This token is used to signal that the `modify_fn` did not produce a new value. It can + be returned by the `modify_fn` to indicate that the current value should remain unchanged. + """ + + def __init__(self, cfg: CurriculumTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + # resolve term configuration + if "address" not in cfg.params: + raise ValueError("The 'address' parameter must be specified in the curriculum term configuration.") + + # store current address + self._address: str = cfg.params["address"] + # store accessor functions + self._get_fn: callable = None + self._set_fn: callable = None + + def __del__(self): + """Destructor to clean up the compiled functions.""" + # clear the getter and setter functions + self._get_fn = None + self._set_fn = None + self._container = None + self._last_path = None + + """ + Operations. + """ + + def __call__( + self, + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + address: str, + modify_fn: callable, + modify_params: dict | None = None, + ): + # fetch the getter and setter functions if not already compiled + if not self._get_fn: + self._get_fn, self._set_fn = self._process_accessors(self._env, self._address) + + # resolve none type + modify_params = {} if modify_params is None else modify_params + + # get the current value of the target attribute + data = self._get_fn() + # modify the value using the provided function + new_val = modify_fn(self._env, env_ids, data, **modify_params) + # set the modified value back to the target attribute + # note: if the modify_fn return NO_CHANGE signal, we do not invoke self.set_fn + if new_val is not self.NO_CHANGE: + self._set_fn(new_val) + + """ + Helper functions. + """ + + def _process_accessors(self, root: ManagerBasedRLEnv, path: str) -> tuple[callable, callable]: + """Process and return the (getter, setter) functions for a dotted attribute path. + + This function resolves a dotted path string to an attribute in the given root object. + The dotted path can include nested attributes, dictionary keys, and sequence indexing. + + For instance, the path "foo.bar[2].baz" would resolve to `root.foo.bar[2].baz`. This + allows accessing attributes in a nested structure, such as a dictionary or a list. + + Args: + root: The main object from which to resolve the attribute. + path: Dotted path string to the attribute variable. For e.g., "foo.bar[2].baz". + + Returns: + A tuple of two functions (getter, setter), where: + the getter retrieves the current value of the attribute, and + the setter writes a new value back to the attribute. + """ + # Turn "a.b[2].c" into ["a", ("b", 2), "c"] and store in parts + path_parts: list[str | tuple[str, int]] = [] + for part in path.split("."): + m = re.compile(r"^(\w+)\[(\d+)\]$").match(part) + if m: + path_parts.append((m.group(1), int(m.group(2)))) + else: + path_parts.append(part) + + # Traverse the parts to find the container + container = root + for container_path in path_parts[:-1]: + if isinstance(container_path, tuple): + # we are accessing a list element + name, idx = container_path + # find underlying attribute + if isinstance(container_path, dict): + seq = container[name] # type: ignore[assignment] + else: + seq = getattr(container, name) + # save the container for the next iteration + container = seq[idx] + else: + # we are accessing a dictionary key or an attribute + if isinstance(container, dict): + container = container[container_path] + else: + container = getattr(container, container_path) + + # save the container and the last part of the path + self._container = container + self._last_path = path_parts[-1] # for "a.b[2].c", this is "c", while for "a.b[2]" it is 2 + + # build the getter and setter + if isinstance(self._container, tuple): + get_value = lambda: self._container[self._last_path] # noqa: E731 + + def set_value(val): + tuple_list = list(self._container) + tuple_list[self._last_path] = val + self._container = tuple(tuple_list) + + elif isinstance(self._container, (list, dict)): + get_value = lambda: self._container[self._last_path] # noqa: E731 + + def set_value(val): + self._container[self._last_path] = val + + elif isinstance(self._container, object): + get_value = lambda: getattr(self._container, self._last_path) # noqa: E731 + set_value = lambda val: setattr(self._container, self._last_path, val) # noqa: E731 + else: + raise TypeError( + f"Unable to build accessors for address '{path}'. Unknown type found for access variable:" + f" '{type(self._container)}'. Expected a list, dict, or object with attributes." + ) + + return get_value, set_value + + +class modify_term_cfg(modify_env_param): + """Curriculum for modifying a manager term configuration at runtime. + + This class inherits from :class:`modify_env_param` and is specifically designed to modify + the configuration of a manager term in the environment. It mainly adds the convenience of + using a simplified address style that uses "s." as a prefix to refer to the manager's configuration. + + For instance, instead of writing "event_manager.cfg.object_physics_material.func.material_buckets", + you can write "events.object_physics_material.func.material_buckets" to refer to the same term configuration. + The same applies to other managers, such as "observations", "commands", "rewards", and "terminations". + + Internally, it replaces the first occurrence of "s." in the address with "_manager.cfg.", + thus transforming the simplified address into a full manager path. + + Usage: + .. code-block:: python + + def override_value(env, env_ids, data, value, num_steps): + if env.common_step_counter > 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 + "modify_fn": override_value, + "modify_params": {"value": (-.75, -.25), "num_steps": 12000} + } + ) + """ + + def __init__(self, cfg, env): + # initialize the parent + super().__init__(cfg, env) + # overwrite the simplified address with the full manager path + self._address = self._address.replace("s.", "_manager.cfg.", 1) diff --git a/source/uwlab/uwlab/envs/mdp/events.py b/source/uwlab/uwlab/envs/mdp/events.py index 30e81e1..e62a291 100644 --- a/source/uwlab/uwlab/envs/mdp/events.py +++ b/source/uwlab/uwlab/envs/mdp/events.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -12,12 +12,11 @@ from isaaclab.managers import SceneEntityCfg if TYPE_CHECKING: - from isaaclab.envs import ManagerBasedEnv - from uwlab.envs import DataManagerBasedRLEnv + from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv def reset_robot_to_default( - env: DataManagerBasedRLEnv, env_ids: torch.Tensor, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot") + env: ManagerBasedRLEnv, env_ids: torch.Tensor, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot") ): """Reset the scene to the default state specified in the scene configuration.""" robot: Articulation = env.scene[robot_cfg.name] diff --git a/source/uwlab/uwlab/envs/mdp/observations.py b/source/uwlab/uwlab/envs/mdp/observations.py index 8b86028..3fc23fd 100644 --- a/source/uwlab/uwlab/envs/mdp/observations.py +++ b/source/uwlab/uwlab/envs/mdp/observations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/rewards.py b/source/uwlab/uwlab/envs/mdp/rewards.py index aed82d7..44acadd 100644 --- a/source/uwlab/uwlab/envs/mdp/rewards.py +++ b/source/uwlab/uwlab/envs/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/mdp/terminations.py b/source/uwlab/uwlab/envs/mdp/terminations.py index 855b4cd..e20afd9 100644 --- a/source/uwlab/uwlab/envs/mdp/terminations.py +++ b/source/uwlab/uwlab/envs/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/envs/real_rl_env.py b/source/uwlab/uwlab/envs/real_rl_env.py index b26039d..8dc3cae 100644 --- a/source/uwlab/uwlab/envs/real_rl_env.py +++ b/source/uwlab/uwlab/envs/real_rl_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,10 +8,10 @@ import math import numpy as np import torch -from typing import TYPE_CHECKING, Any, ClassVar, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, ClassVar import isaacsim.core.utils.torch as torch_utils - from isaaclab.envs import ManagerBasedRLEnv from isaaclab.envs.common import VecEnvObs, VecEnvStepReturn from isaaclab.managers import ( diff --git a/source/uwlab/uwlab/envs/real_rl_env_cfg.py b/source/uwlab/uwlab/envs/real_rl_env_cfg.py index 4e0ee35..a9a4263 100644 --- a/source/uwlab/uwlab/envs/real_rl_env_cfg.py +++ b/source/uwlab/uwlab/envs/real_rl_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -7,6 +7,7 @@ from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.utils import configclass + from uwlab.scene import SceneContextCfg from .real_rl_env import RealRLEnv diff --git a/source/uwlab/uwlab/envs/ui/__init__.py b/source/uwlab/uwlab/envs/ui/__init__.py new file mode 100644 index 0000000..dae4a4d --- /dev/null +++ b/source/uwlab/uwlab/envs/ui/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module providing UI window implementation for environments. + +The UI elements are used to control the environment and visualize the state of the environment. +This includes functionalities such as tracking a robot in the simulation, +toggling different debug visualization tools, and other user-defined functionalities. +""" + +from .base_env_window import BaseEnvWindow +from .empty_window import EmptyWindow +from .manager_based_rl_env_window import ManagerBasedRLEnvWindow +from .viewport_camera_controller import ViewportCameraController diff --git a/source/uwlab/uwlab/envs/ui/base_env_window.py b/source/uwlab/uwlab/envs/ui/base_env_window.py new file mode 100644 index 0000000..eea06c1 --- /dev/null +++ b/source/uwlab/uwlab/envs/ui/base_env_window.py @@ -0,0 +1,457 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import asyncio +import os +import weakref +from datetime import datetime +from typing import TYPE_CHECKING + +import isaacsim +import omni.kit.app +import omni.kit.commands +import omni.usd +from isaaclab.ui.widgets import ManagerLiveVisualizer +from isaacsim.core.utils.stage import get_current_stage +from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics + +if TYPE_CHECKING: + import omni.ui + + from ..manager_based_env import ManagerBasedEnv + + +class BaseEnvWindow: + """Window manager for the basic environment. + + This class creates a window that is used to control the environment. The window + contains controls for rendering, debug visualization, and other environment-specific + UI elements. + + Users can add their own UI elements to the window by using the `with` context manager. + This can be done either be inheriting the class or by using the `env.window` object + directly from the standalone execution script. + + Example for adding a UI element from the standalone execution script: + >>> with env.window.ui_window_elements["main_vstack"]: + >>> ui.Label("My UI element") + + """ + + def __init__(self, env: ManagerBasedEnv, window_name: str = "IsaacLab"): + """Initialize the window. + + Args: + env: The environment object. + window_name: The name of the window. Defaults to "IsaacLab". + """ + # store inputs + self.env = env + # prepare the list of assets that can be followed by the viewport camera + # note that the first two options are "World" and "Env" which are special cases + self._viewer_assets_options = [ + "World", + "Env", + *self.env.scene.rigid_objects.keys(), + *self.env.scene.articulations.keys(), + ] + + # get stage handle + self.stage = get_current_stage() + + # Listeners for environment selection changes + self._ui_listeners: list[ManagerLiveVisualizer] = [] + + print("Creating window for environment.") + # create window for UI + self.ui_window = omni.ui.Window( + window_name, width=400, height=500, visible=True, dock_preference=omni.ui.DockPreference.RIGHT_TOP + ) + # dock next to properties window + asyncio.ensure_future(self._dock_window(window_title=self.ui_window.title)) + + # keep a dictionary of stacks so that child environments can add their own UI elements + # this can be done by using the `with` context manager + self.ui_window_elements = dict() + # create main frame + self.ui_window_elements["main_frame"] = self.ui_window.frame + with self.ui_window_elements["main_frame"]: + # create main stack + self.ui_window_elements["main_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["main_vstack"]: + # create collapsable frame for simulation + self._build_sim_frame() + # create collapsable frame for viewer + self._build_viewer_frame() + # create collapsable frame for debug visualization + self._build_debug_vis_frame() + with self.ui_window_elements["debug_frame"]: + with self.ui_window_elements["debug_vstack"]: + self._visualize_manager(title="Actions", class_name="action_manager") + self._visualize_manager(title="Observations", class_name="observation_manager") + + def __del__(self): + """Destructor for the window.""" + # destroy the window + if self.ui_window is not None: + self.ui_window.visible = False + self.ui_window.destroy() + self.ui_window = None + + """ + Build sub-sections of the UI. + """ + + def _build_sim_frame(self): + """Builds the sim-related controls frame for the UI.""" + # create collapsable frame for controls + self.ui_window_elements["sim_frame"] = omni.ui.CollapsableFrame( + title="Simulation Settings", + width=omni.ui.Fraction(1), + height=0, + collapsed=False, + style=isaacsim.gui.components.ui_utils.get_style(), + horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, + vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + ) + with self.ui_window_elements["sim_frame"]: + # create stack for controls + self.ui_window_elements["sim_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["sim_vstack"]: + # create rendering mode dropdown + render_mode_cfg = { + "label": "Rendering Mode", + "type": "dropdown", + "default_val": self.env.sim.render_mode.value, + "items": [member.name for member in self.env.sim.RenderMode if member.value >= 0], + "tooltip": "Select a rendering mode\n" + self.env.sim.RenderMode.__doc__, + "on_clicked_fn": lambda value: self.env.sim.set_render_mode(self.env.sim.RenderMode[value]), + } + self.ui_window_elements["render_dropdown"] = isaacsim.gui.components.ui_utils.dropdown_builder( + **render_mode_cfg + ) + + # create animation recording box + record_animate_cfg = { + "label": "Record Animation", + "type": "state_button", + "a_text": "START", + "b_text": "STOP", + "tooltip": "Record the animation of the scene. Only effective if fabric is disabled.", + "on_clicked_fn": lambda value: self._toggle_recording_animation_fn(value), + } + self.ui_window_elements["record_animation"] = isaacsim.gui.components.ui_utils.state_btn_builder( + **record_animate_cfg + ) + # disable the button if fabric is not enabled + self.ui_window_elements["record_animation"].enabled = not self.env.sim.is_fabric_enabled() + + def _build_viewer_frame(self): + """Build the viewer-related control frame for the UI.""" + # create collapsable frame for viewer + self.ui_window_elements["viewer_frame"] = omni.ui.CollapsableFrame( + title="Viewer Settings", + width=omni.ui.Fraction(1), + height=0, + collapsed=False, + style=isaacsim.gui.components.ui_utils.get_style(), + horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, + vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + ) + with self.ui_window_elements["viewer_frame"]: + # create stack for controls + self.ui_window_elements["viewer_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["viewer_vstack"]: + # create a number slider to move to environment origin + # NOTE: slider is 1-indexed, whereas the env index is 0-indexed + viewport_origin_cfg = { + "label": "Environment Index", + "type": "button", + "default_val": self.env.cfg.viewer.env_index + 1, + "min": 1, + "max": self.env.num_envs, + "tooltip": "The environment index to follow. Only effective if follow mode is not 'World'.", + } + self.ui_window_elements["viewer_env_index"] = isaacsim.gui.components.ui_utils.int_builder( + **viewport_origin_cfg + ) + # create a number slider to move to environment origin + self.ui_window_elements["viewer_env_index"].add_value_changed_fn(self._set_viewer_env_index_fn) + + # create a tracker for the camera location + viewer_follow_cfg = { + "label": "Follow Mode", + "type": "dropdown", + "default_val": 0, + "items": [name.replace("_", " ").title() for name in self._viewer_assets_options], + "tooltip": "Select the viewport camera following mode.", + "on_clicked_fn": self._set_viewer_origin_type_fn, + } + self.ui_window_elements["viewer_follow"] = isaacsim.gui.components.ui_utils.dropdown_builder( + **viewer_follow_cfg + ) + + # add viewer default eye and lookat locations + self.ui_window_elements["viewer_eye"] = isaacsim.gui.components.ui_utils.xyz_builder( + label="Camera Eye", + tooltip="Modify the XYZ location of the viewer eye.", + default_val=self.env.cfg.viewer.eye, + step=0.1, + on_value_changed_fn=[self._set_viewer_location_fn] * 3, + ) + self.ui_window_elements["viewer_lookat"] = isaacsim.gui.components.ui_utils.xyz_builder( + label="Camera Target", + tooltip="Modify the XYZ location of the viewer target.", + default_val=self.env.cfg.viewer.lookat, + step=0.1, + on_value_changed_fn=[self._set_viewer_location_fn] * 3, + ) + + def _build_debug_vis_frame(self): + """Builds the debug visualization frame for various scene elements. + + This function inquires the scene for all elements that have a debug visualization + implemented and creates a checkbox to toggle the debug visualization for each element + that has it implemented. If the element does not have a debug visualization implemented, + a label is created instead. + """ + # create collapsable frame for debug visualization + self.ui_window_elements["debug_frame"] = omni.ui.CollapsableFrame( + title="Scene Debug Visualization", + width=omni.ui.Fraction(1), + height=0, + collapsed=False, + style=isaacsim.gui.components.ui_utils.get_style(), + horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, + vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + ) + with self.ui_window_elements["debug_frame"]: + # create stack for debug visualization + self.ui_window_elements["debug_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["debug_vstack"]: + elements = [ + self.env.scene.terrain, + *self.env.scene.rigid_objects.values(), + *self.env.scene.articulations.values(), + *self.env.scene.sensors.values(), + ] + names = [ + "terrain", + *self.env.scene.rigid_objects.keys(), + *self.env.scene.articulations.keys(), + *self.env.scene.sensors.keys(), + ] + # create one for the terrain + for elem, name in zip(elements, names): + if elem is not None: + self._create_debug_vis_ui_element(name, elem) + + def _visualize_manager(self, title: str, class_name: str) -> None: + """Checks if the attribute with the name 'class_name' can be visualized. If yes, create vis interface. + + Args: + title: The title of the manager visualization frame. + class_name: The name of the manager to visualize. + """ + + if hasattr(self.env, class_name) and class_name in self.env.manager_visualizers: + manager = self.env.manager_visualizers[class_name] + if hasattr(manager, "has_debug_vis_implementation"): + self._create_debug_vis_ui_element(title, manager) + else: + print( + f"ManagerLiveVisualizer cannot be created for manager: {class_name}, has_debug_vis_implementation" + " does not exist" + ) + else: + print(f"ManagerLiveVisualizer cannot be created for manager: {class_name}, Manager does not exist") + + """ + Custom callbacks for UI elements. + """ + + def _toggle_recording_animation_fn(self, value: bool): + """Toggles the animation recording.""" + if value: + # log directory to save the recording + if not hasattr(self, "animation_log_dir"): + # create a new log directory + log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + self.animation_log_dir = os.path.join(os.getcwd(), "recordings", log_dir) + # start the recording + _ = omni.kit.commands.execute( + "StartRecording", + target_paths=[("/World", True)], + live_mode=True, + use_frame_range=False, + start_frame=0, + end_frame=0, + use_preroll=False, + preroll_frame=0, + record_to="FILE", + fps=0, + apply_root_anim=False, + increment_name=True, + record_folder=self.animation_log_dir, + take_name="TimeSample", + ) + else: + # stop the recording + _ = omni.kit.commands.execute("StopRecording") + # save the current stage + source_layer = self.stage.GetRootLayer() + # output the stage to a file + stage_usd_path = os.path.join(self.animation_log_dir, "Stage.usd") + source_prim_path = "/" + # creates empty anon layer + temp_layer = Sdf.Find(stage_usd_path) + if temp_layer is None: + temp_layer = Sdf.Layer.CreateNew(stage_usd_path) + temp_stage = Usd.Stage.Open(temp_layer) + # update stage data + UsdGeom.SetStageUpAxis(temp_stage, UsdGeom.GetStageUpAxis(self.stage)) + UsdGeom.SetStageMetersPerUnit(temp_stage, UsdGeom.GetStageMetersPerUnit(self.stage)) + # copy the prim + Sdf.CreatePrimInLayer(temp_layer, source_prim_path) + Sdf.CopySpec(source_layer, source_prim_path, temp_layer, source_prim_path) + # set the default prim + temp_layer.defaultPrim = Sdf.Path(source_prim_path).name + # remove all physics from the stage + for prim in temp_stage.TraverseAll(): + # skip if the prim is an instance + if prim.IsInstanceable(): + continue + # if prim has articulation then disable it + if prim.HasAPI(UsdPhysics.ArticulationRootAPI): + prim.RemoveAPI(UsdPhysics.ArticulationRootAPI) + prim.RemoveAPI(PhysxSchema.PhysxArticulationAPI) + # if prim has rigid body then disable it + if prim.HasAPI(UsdPhysics.RigidBodyAPI): + prim.RemoveAPI(UsdPhysics.RigidBodyAPI) + prim.RemoveAPI(PhysxSchema.PhysxRigidBodyAPI) + # if prim is a joint type then disable it + if prim.IsA(UsdPhysics.Joint): + prim.GetAttribute("physics:jointEnabled").Set(False) + # resolve all paths relative to layer path + omni.usd.resolve_paths(source_layer.identifier, temp_layer.identifier) + # save the stage + temp_layer.Save() + # print the path to the saved stage + print("Recording completed.") + print(f"\tSaved recorded stage to : {stage_usd_path}") + print(f"\tSaved recorded animation to: {os.path.join(self.animation_log_dir, 'TimeSample_tk001.usd')}") + print("\nTo play the animation, check the instructions in the following link:") + print( + "\thttps://docs.omniverse.nvidia.com/extensions/latest/ext_animation_stage-recorder.html#using-the-captured-timesamples" + ) + print("\n") + # reset the log directory + self.animation_log_dir = None + + def _set_viewer_origin_type_fn(self, value: str): + """Sets the origin of the viewport's camera. This is based on the drop-down menu in the UI.""" + # Extract the viewport camera controller from environment + vcc = self.env.viewport_camera_controller + if vcc is None: + raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") + + # Based on origin type, update the camera view + if value == "World": + vcc.update_view_to_world() + elif value == "Env": + vcc.update_view_to_env() + else: + # find which index the asset is + fancy_names = [name.replace("_", " ").title() for name in self._viewer_assets_options] + # store the desired env index + viewer_asset_name = self._viewer_assets_options[fancy_names.index(value)] + # update the camera view + vcc.update_view_to_asset_root(viewer_asset_name) + + def _set_viewer_location_fn(self, model: omni.ui.SimpleFloatModel): + """Sets the viewport camera location based on the UI.""" + # access the viewport camera controller (for brevity) + vcc = self.env.viewport_camera_controller + if vcc is None: + raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") + # obtain the camera locations and set them in the viewpoint camera controller + eye = [self.ui_window_elements["viewer_eye"][i].get_value_as_float() for i in range(3)] + lookat = [self.ui_window_elements["viewer_lookat"][i].get_value_as_float() for i in range(3)] + # update the camera view + vcc.update_view_location(eye, lookat) + + def _set_viewer_env_index_fn(self, model: omni.ui.SimpleIntModel): + """Sets the environment index and updates the camera if in 'env' origin mode.""" + # access the viewport camera controller (for brevity) + vcc = self.env.viewport_camera_controller + if vcc is None: + raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") + # store the desired env index, UI is 1-indexed + vcc.set_view_env_index(model.as_int - 1) + # notify additional listeners + for listener in self._ui_listeners: + listener.set_env_selection(model.as_int - 1) + + """ + Helper functions - UI building. + """ + + def _create_debug_vis_ui_element(self, name: str, elem: object): + """Create a checkbox for toggling debug visualization for the given element.""" + from omni.kit.window.extensions import SimpleCheckBox + + with omni.ui.HStack(): + # create the UI element + text = ( + "Toggle debug visualization." + if elem.has_debug_vis_implementation + else "Debug visualization not implemented." + ) + omni.ui.Label( + name.replace("_", " ").title(), + width=isaacsim.gui.components.ui_utils.LABEL_WIDTH - 12, + alignment=omni.ui.Alignment.LEFT_CENTER, + tooltip=text, + ) + has_cfg = hasattr(elem, "cfg") and elem.cfg is not None + is_checked = False + if has_cfg: + is_checked = (hasattr(elem.cfg, "debug_vis") and elem.cfg.debug_vis) or ( + hasattr(elem, "debug_vis") and elem.debug_vis + ) + self.ui_window_elements[f"{name}_cb"] = SimpleCheckBox( + model=omni.ui.SimpleBoolModel(), + enabled=elem.has_debug_vis_implementation, + checked=is_checked, + on_checked_fn=lambda value, e=weakref.proxy(elem): e.set_debug_vis(value), + ) + isaacsim.gui.components.ui_utils.add_line_rect_flourish() + + # Create a panel for the debug visualization + if isinstance(elem, ManagerLiveVisualizer): + self.ui_window_elements[f"{name}_panel"] = omni.ui.Frame(width=omni.ui.Fraction(1)) + if not elem.set_vis_frame(self.ui_window_elements[f"{name}_panel"]): + print(f"Frame failed to set for ManagerLiveVisualizer: {name}") + + # Add listener for environment selection changes + if isinstance(elem, ManagerLiveVisualizer): + self._ui_listeners.append(elem) + + async def _dock_window(self, window_title: str): + """Docks the custom UI window to the property window.""" + # wait for the window to be created + for _ in range(5): + if omni.ui.Workspace.get_window(window_title): + break + await self.env.sim.app.next_update_async() + + # dock next to properties window + custom_window = omni.ui.Workspace.get_window(window_title) + property_window = omni.ui.Workspace.get_window("Property") + if custom_window and property_window: + custom_window.dock_in(property_window, omni.ui.DockPosition.SAME, 1.0) + custom_window.focus() diff --git a/source/uwlab/uwlab/envs/ui/empty_window.py b/source/uwlab/uwlab/envs/ui/empty_window.py new file mode 100644 index 0000000..2d45fe6 --- /dev/null +++ b/source/uwlab/uwlab/envs/ui/empty_window.py @@ -0,0 +1,79 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import omni.kit.app + +if TYPE_CHECKING: + import omni.ui + + from ..manager_based_env import ManagerBasedEnv + + +class EmptyWindow: + """ + Creates an empty UI window that can be docked in the Omniverse Kit environment. + + The class initializes a dockable UI window and provides a main frame with a vertical stack. + You can add custom UI elements to this vertical stack. + + Example for adding a UI element from the standalone execution script: + >>> with env.window.ui_window_elements["main_vstack"]: + >>> ui.Label("My UI element") + + """ + + def __init__(self, env: ManagerBasedEnv, window_name: str): + """Initialize the window. + + Args: + env: The environment object. + window_name: The name of the window. + """ + # store environment + self.env = env + + # create window for UI + self.ui_window = omni.ui.Window( + window_name, width=400, height=500, visible=True, dock_preference=omni.ui.DockPreference.RIGHT_TOP + ) + # dock next to properties window + asyncio.ensure_future(self._dock_window(window_title=self.ui_window.title)) + + # keep a dictionary of stacks so that child environments can add their own UI elements + # this can be done by using the `with` context manager + self.ui_window_elements = dict() + # create main frame + self.ui_window_elements["main_frame"] = self.ui_window.frame + with self.ui_window_elements["main_frame"]: + # create main vstack + self.ui_window_elements["main_vstack"] = omni.ui.VStack(spacing=5, height=0) + + def __del__(self): + """Destructor for the window.""" + # destroy the window + if self.ui_window is not None: + self.ui_window.visible = False + self.ui_window.destroy() + self.ui_window = None + + async def _dock_window(self, window_title: str): + """Docks the custom UI window to the property window.""" + # wait for the window to be created + for _ in range(5): + if omni.ui.Workspace.get_window(window_title): + break + await self.env.sim.app.next_update_async() + + # dock next to properties window + custom_window = omni.ui.Workspace.get_window(window_title) + property_window = omni.ui.Workspace.get_window("Property") + if custom_window and property_window: + custom_window.dock_in(property_window, omni.ui.DockPosition.SAME, 1.0) + custom_window.focus() diff --git a/source/uwlab/uwlab/envs/ui/manager_based_rl_env_window.py b/source/uwlab/uwlab/envs/ui/manager_based_rl_env_window.py new file mode 100644 index 0000000..100925c --- /dev/null +++ b/source/uwlab/uwlab/envs/ui/manager_based_rl_env_window.py @@ -0,0 +1,40 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base_env_window import BaseEnvWindow + +if TYPE_CHECKING: + from ..manager_based_rl_env import ManagerBasedRLEnv + + +class ManagerBasedRLEnvWindow(BaseEnvWindow): + """Window manager for the RL environment. + + On top of the basic environment window, this class adds controls for the RL environment. + This includes visualization of the command manager. + """ + + def __init__(self, env: ManagerBasedRLEnv, window_name: str = "IsaacLab"): + """Initialize the window. + + Args: + env: The environment object. + window_name: The name of the window. Defaults to "IsaacLab". + """ + # initialize base window + super().__init__(env, window_name) + + # add custom UI elements + with self.ui_window_elements["main_vstack"]: + with self.ui_window_elements["debug_frame"]: + with self.ui_window_elements["debug_vstack"]: + self._visualize_manager(title="Commands", class_name="command_manager") + self._visualize_manager(title="Rewards", class_name="reward_manager") + self._visualize_manager(title="Curriculum", class_name="curriculum_manager") + self._visualize_manager(title="Termination", class_name="termination_manager") diff --git a/source/uwlab/uwlab/envs/ui/viewport_camera_controller.py b/source/uwlab/uwlab/envs/ui/viewport_camera_controller.py new file mode 100644 index 0000000..011f861 --- /dev/null +++ b/source/uwlab/uwlab/envs/ui/viewport_camera_controller.py @@ -0,0 +1,231 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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 omni.kit.app +import omni.timeline +from isaaclab.assets.articulation.articulation import Articulation + +if TYPE_CHECKING: + from isaaclab.envs import DirectRLEnv, ManagerBasedEnv, ViewerCfg + + +class ViewportCameraController: + """This class handles controlling the camera associated with a viewport in the simulator. + + It can be used to set the viewpoint camera to track different origin types: + + - **world**: the center of the world (static) + - **env**: the center of an environment (static) + - **asset_root**: the root of an asset in the scene (e.g. tracking a robot moving in the scene) + + On creation, the camera is set to track the origin type specified in the configuration. + + For the :attr:`asset_root` origin type, the camera is updated at each rendering step to track the asset's + root position. For this, it registers a callback to the post update event stream from the simulation app. + """ + + def __init__(self, env: ManagerBasedEnv | DirectRLEnv, cfg: ViewerCfg): + """Initialize the ViewportCameraController. + + Args: + env: The environment. + cfg: The configuration for the viewport camera controller. + + Raises: + ValueError: If origin type is configured to be "env" but :attr:`cfg.env_index` is out of bounds. + ValueError: If origin type is configured to be "asset_root" but :attr:`cfg.asset_name` is unset. + + """ + # store inputs + self._env = env + self._cfg = copy.deepcopy(cfg) + # cast viewer eye and look-at to numpy arrays + self.default_cam_eye = np.array(self._cfg.eye, dtype=float) + self.default_cam_lookat = np.array(self._cfg.lookat, dtype=float) + + # set the camera origins + if self.cfg.origin_type == "env": + # check that the env_index is within bounds + self.set_view_env_index(self.cfg.env_index) + # set the camera origin to the center of the environment + self.update_view_to_env() + elif self.cfg.origin_type == "asset_root" or self.cfg.origin_type == "asset_body": + # note: we do not yet update camera for tracking an asset origin, as the asset may not yet be + # in the scene when this is called. Instead, we subscribe to the post update event to update the camera + # at each rendering step. + if self.cfg.asset_name is None: + raise ValueError(f"No asset name provided for viewer with origin type: '{self.cfg.origin_type}'.") + if self.cfg.origin_type == "asset_body": + if self.cfg.body_name is None: + raise ValueError(f"No body name provided for viewer with origin type: '{self.cfg.origin_type}'.") + else: + # set the camera origin to the center of the world + self.update_view_to_world() + + # subscribe to post update event so that camera view can be updated at each rendering step + app_interface = omni.kit.app.get_app_interface() + app_event_stream = app_interface.get_post_update_event_stream() + self._viewport_camera_update_handle = app_event_stream.create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._update_tracking_callback(event) + ) + + def __del__(self): + """Unsubscribe from the callback.""" + # use hasattr to handle case where __init__ has not completed before __del__ is called + if hasattr(self, "_viewport_camera_update_handle") and self._viewport_camera_update_handle is not None: + self._viewport_camera_update_handle.unsubscribe() + self._viewport_camera_update_handle = None + + """ + Properties + """ + + @property + def cfg(self) -> ViewerCfg: + """The configuration for the viewer.""" + return self._cfg + + """ + Public Functions + """ + + def set_view_env_index(self, env_index: int): + """Sets the environment index for the camera view. + + Args: + env_index: The index of the environment to set the camera view to. + + Raises: + ValueError: If the environment index is out of bounds. It should be between 0 and num_envs - 1. + """ + # check that the env_index is within bounds + if env_index < 0 or env_index >= self._env.num_envs: + raise ValueError( + f"Out of range value for attribute 'env_index': {env_index}." + f" Expected a value between 0 and {self._env.num_envs - 1} for the current environment." + ) + # update the environment index + self.cfg.env_index = env_index + # update the camera view if the origin is set to env type (since, the camera view is static) + # note: for assets, the camera view is updated at each rendering step + if self.cfg.origin_type == "env": + self.update_view_to_env() + + def update_view_to_world(self): + """Updates the viewer's origin to the origin of the world which is (0, 0, 0).""" + # set origin type to world + self.cfg.origin_type = "world" + # update the camera origins + self.viewer_origin = torch.zeros(3) + # update the camera view + self.update_view_location() + + def update_view_to_env(self): + """Updates the viewer's origin to the origin of the selected environment.""" + # set origin type to world + self.cfg.origin_type = "env" + # update the camera origins + self.viewer_origin = self._env.scene.env_origins[self.cfg.env_index] + # update the camera view + self.update_view_location() + + def update_view_to_asset_root(self, asset_name: str): + """Updates the viewer's origin based upon the root of an asset in the scene. + + Args: + asset_name: The name of the asset in the scene. The name should match the name of the + asset in the scene. + + Raises: + ValueError: If the asset is not in the scene. + """ + # check if the asset is in the scene + if self.cfg.asset_name != asset_name: + asset_entities = [*self._env.scene.rigid_objects.keys(), *self._env.scene.articulations.keys()] + if asset_name not in asset_entities: + raise ValueError(f"Asset '{asset_name}' is not in the scene. Available entities: {asset_entities}.") + # update the asset name + self.cfg.asset_name = asset_name + # set origin type to asset_root + self.cfg.origin_type = "asset_root" + # update the camera origins + self.viewer_origin = self._env.scene[self.cfg.asset_name].data.root_pos_w[self.cfg.env_index] + # update the camera view + self.update_view_location() + + def update_view_to_asset_body(self, asset_name: str, body_name: str): + """Updates the viewer's origin based upon the body of an asset in the scene. + + Args: + asset_name: The name of the asset in the scene. The name should match the name of the + asset in the scene. + body_name: The name of the body in the asset. + + Raises: + ValueError: If the asset is not in the scene or the body is not valid. + """ + # check if the asset is in the scene + if self.cfg.asset_name != asset_name: + asset_entities = [*self._env.scene.rigid_objects.keys(), *self._env.scene.articulations.keys()] + if asset_name not in asset_entities: + raise ValueError(f"Asset '{asset_name}' is not in the scene. Available entities: {asset_entities}.") + # check if the body is in the asset + asset: Articulation = self._env.scene[asset_name] + if body_name not in asset.body_names: + raise ValueError( + f"'{body_name}' is not a body of Asset '{asset_name}'. Available bodies: {asset.body_names}." + ) + # get the body index + body_id, _ = asset.find_bodies(body_name) + # update the asset name + self.cfg.asset_name = asset_name + # set origin type to asset_body + self.cfg.origin_type = "asset_body" + # update the camera origins + self.viewer_origin = self._env.scene[self.cfg.asset_name].data.body_pos_w[self.cfg.env_index, body_id].view(3) + # update the camera view + self.update_view_location() + + def update_view_location(self, eye: Sequence[float] | None = None, lookat: Sequence[float] | None = None): + """Updates the camera view pose based on the current viewer origin and the eye and lookat positions. + + Args: + eye: The eye position of the camera. If None, the current eye position is used. + lookat: The lookat position of the camera. If None, the current lookat position is used. + """ + # store the camera view pose for later use + if eye is not None: + self.default_cam_eye = np.asarray(eye, dtype=float) + if lookat is not None: + self.default_cam_lookat = np.asarray(lookat, dtype=float) + # set the camera locations + viewer_origin = self.viewer_origin.detach().cpu().numpy() + cam_eye = viewer_origin + self.default_cam_eye + cam_target = viewer_origin + self.default_cam_lookat + + # set the camera view + self._env.sim.set_camera_view(eye=cam_eye, target=cam_target) + + """ + Private Functions + """ + + def _update_tracking_callback(self, event): + """Updates the camera view at each rendering step.""" + # update the camera view if the origin is set to asset_root + # in other cases, the camera view is static and does not need to be updated continuously + if self.cfg.origin_type == "asset_root" and self.cfg.asset_name is not None: + self.update_view_to_asset_root(self.cfg.asset_name) + if self.cfg.origin_type == "asset_body" and self.cfg.asset_name is not None and self.cfg.body_name is not None: + self.update_view_to_asset_body(self.cfg.asset_name, self.cfg.body_name) diff --git a/source/uwlab/uwlab/genes/__init__.py b/source/uwlab/uwlab/genes/__init__.py index e050c05..082c14d 100644 --- a/source/uwlab/uwlab/genes/__init__.py +++ b/source/uwlab/uwlab/genes/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/genes/gene/__init__.py b/source/uwlab/uwlab/genes/gene/__init__.py index 4216f92..4390f1b 100644 --- a/source/uwlab/uwlab/genes/gene/__init__.py +++ b/source/uwlab/uwlab/genes/gene/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/genes/gene/gene.py b/source/uwlab/uwlab/genes/gene/gene.py index 6053b96..5e9b210 100644 --- a/source/uwlab/uwlab/genes/gene/gene.py +++ b/source/uwlab/uwlab/genes/gene/gene.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/genes/gene/gene_cfg.py b/source/uwlab/uwlab/genes/gene/gene_cfg.py index fc74b81..620deef 100644 --- a/source/uwlab/uwlab/genes/gene/gene_cfg.py +++ b/source/uwlab/uwlab/genes/gene/gene_cfg.py @@ -1,14 +1,16 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable, Literal +from typing import Literal from isaaclab.utils import configclass + from uwlab.genes.gene import gene diff --git a/source/uwlab/uwlab/genes/gene/gene_mdp.py b/source/uwlab/uwlab/genes/gene/gene_mdp.py index b7bf355..54af6ae 100644 --- a/source/uwlab/uwlab/genes/gene/gene_mdp.py +++ b/source/uwlab/uwlab/genes/gene/gene_mdp.py @@ -1,13 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import numpy as np import torch -from typing import List - -from uwlab.terrains.terrain_generator_cfg import MultiOriginTerrainGeneratorCfg # MUTATION FUNCTIONS @@ -47,7 +44,7 @@ def random_dict(rng: np.random.Generator, val, mutation_rate: float, dict: dict) return {key: value} -def mutate_terrain_cfg(rng: np.random.Generator, val, mutation_rate, cfg: MultiOriginTerrainGeneratorCfg): +def mutate_terrain_cfg(rng: np.random.Generator, val, mutation_rate, cfg): key = random_selection(rng, val, mutation_rate, list(cfg.sub_terrains.keys())) value = cfg.sub_terrains[key] sub_terrain = {key: value} @@ -58,7 +55,7 @@ def mutate_terrain_cfg(rng: np.random.Generator, val, mutation_rate, cfg: MultiO # BREEDING FUNCTIONS -def breed_terrain_cfg(this_val: MultiOriginTerrainGeneratorCfg, other_val: MultiOriginTerrainGeneratorCfg): +def breed_terrain_cfg(this_val, other_val): num_sub_terrains = len(this_val.sub_terrains) + len(other_val.sub_terrains) width = np.ceil(np.sqrt(num_sub_terrains)) this_val.num_cols = width @@ -67,11 +64,11 @@ def breed_terrain_cfg(this_val: MultiOriginTerrainGeneratorCfg, other_val: Multi def value_distribution( - values: List[float], + values: list[float], distribute_to_n_values: int, value_to_distribute: float | None = None, equal_distribution: bool = False, -) -> List[float]: +) -> list[float]: """Redistributes the total sum of values to the top n values based on their initial proportion.""" if distribute_to_n_values <= 0 or distribute_to_n_values > len(values): raise ValueError("distribute_to_n_values must be greater than 0 and less than or equal to the length of values") @@ -94,7 +91,7 @@ def value_distribution( return output.tolist() -def probability_distribution(vals: List[float], distribute_to_n_values: int) -> List[float]: +def probability_distribution(vals: list[float], distribute_to_n_values: int) -> list[float]: """Converts redistributed values into a valid probability distribution using softmax and returns it as a list of floats.""" # First, redistribute the values redistributed_vals = value_distribution(vals, distribute_to_n_values) diff --git a/source/uwlab/uwlab/genes/genome.py b/source/uwlab/uwlab/genes/genome.py index 667dd35..f407087 100644 --- a/source/uwlab/uwlab/genes/genome.py +++ b/source/uwlab/uwlab/genes/genome.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,10 +10,10 @@ from typing import TYPE_CHECKING from isaaclab.envs import ManagerBasedRLEnvCfg -from uwlab.genes.gene import GeneOperatorBase, GeneOperatorBaseCfg, TupleGeneBaseCfg - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg +from uwlab.genes.gene import GeneOperatorBase, GeneOperatorBaseCfg, TupleGeneBaseCfg + if TYPE_CHECKING: from .genome_cfg import GenomeCfg diff --git a/source/uwlab/uwlab/genes/genome_cfg.py b/source/uwlab/uwlab/genes/genome_cfg.py index 2b03472..7274b2d 100644 --- a/source/uwlab/uwlab/genes/genome_cfg.py +++ b/source/uwlab/uwlab/genes/genome_cfg.py @@ -1,12 +1,12 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable from isaaclab.utils import configclass diff --git a/source/uwlab/uwlab/managers/__init__.py b/source/uwlab/uwlab/managers/__init__.py index f4a9243..fd8b9bd 100644 --- a/source/uwlab/uwlab/managers/__init__.py +++ b/source/uwlab/uwlab/managers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/managers/data_manager.py b/source/uwlab/uwlab/managers/data_manager.py index d7d2517..3fdef02 100644 --- a/source/uwlab/uwlab/managers/data_manager.py +++ b/source/uwlab/uwlab/managers/data_manager.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -21,7 +16,6 @@ from typing import TYPE_CHECKING import omni.kit.app - from isaaclab.managers.manager_base import ManagerBase, ManagerTermBase from .manager_term_cfg import DataTermCfg diff --git a/source/uwlab/uwlab/managers/manager_term_cfg.py b/source/uwlab/uwlab/managers/manager_term_cfg.py index d6ac934..13e6b0b 100644 --- a/source/uwlab/uwlab/managers/manager_term_cfg.py +++ b/source/uwlab/uwlab/managers/manager_term_cfg.py @@ -1,61 +1,30 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from collections.abc import Callable -from dataclasses import MISSING - -from isaaclab.managers.manager_term_cfg import ManagerTermBaseCfg -from isaaclab.utils import configclass -from isaaclab.utils.noise import NoiseCfg - - -@configclass -class DataTermCfg(ManagerTermBaseCfg): - """Configuration for an observation term.""" - - func: Callable[..., dict] = MISSING - """The name of the function to be called. - - This function should take the environment object and any other parameters - as input and return the observation signal as torch float tensors of - shape (num_envs, obs_term_dim). - """ +"""Configuration terms for different managers.""" - noise: NoiseCfg | None = None - """The noise to add to the observation. Defaults to None, in which case no noise is added.""" +from __future__ import annotations - clip: tuple[float, float] | None = None - """The clipping range for the observation after adding noise. Defaults to None, - in which case no clipping is applied.""" +from dataclasses import MISSING +from typing import TYPE_CHECKING - scale: float | None = None - """The scale to apply to the observation after clipping. Defaults to None, - in which case no scaling is applied (same as setting scale to :obj:`1`).""" +from isaaclab.utils import configclass - history_length: int = 1 +if TYPE_CHECKING: + from .data_manager import DataTerm @configclass -class DataGroupCfg: - """Configuration for an observation group.""" +class DataTermCfg: + """Configuration for a data generator term.""" - concatenate_terms: bool = True - """Whether to concatenate the observation terms in the group. Defaults to True. + class_type: type[DataTerm] = MISSING + """The associated data term class to use. - If true, the observation terms in the group are concatenated along the last dimension. - Otherwise, they are kept separate and returned as a dictionary. + The class should inherit from :class:`isaaclab.managers.data_manager.DataTerm`. """ - enable_corruption: bool = False - """Whether to enable corruption for the observation group. Defaults to False. - - If true, the observation terms in the group are corrupted by adding noise (if specified). - Otherwise, no corruption is applied. - """ + debug_vis: bool = False + """Whether to visualize debug information. Defaults to False.""" diff --git a/source/uwlab/uwlab/scene/__init__.py b/source/uwlab/uwlab/scene/__init__.py index c764cf6..26f60f8 100644 --- a/source/uwlab/uwlab/scene/__init__.py +++ b/source/uwlab/uwlab/scene/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -25,7 +25,5 @@ :mod:`isaaclab.managers` sub-package for more details. """ -from .interactive_scene import InteractiveScene -from .interactive_scene_cfg import InteractiveSceneCfg from .scene_context import SceneContext from .scene_context_cfg import SceneContextCfg diff --git a/source/uwlab/uwlab/scene/interactive_scene.py b/source/uwlab/uwlab/scene/interactive_scene.py deleted file mode 100644 index fde0e67..0000000 --- a/source/uwlab/uwlab/scene/interactive_scene.py +++ /dev/null @@ -1,648 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -import torch -from collections.abc import Sequence -from typing import Any - -import carb -import omni.usd -from isaacsim.core.cloner import GridCloner -from isaacsim.core.prims import XFormPrim -from pxr import PhysxSchema - -import isaaclab.sim as sim_utils -from isaaclab.assets import ( - Articulation, - ArticulationCfg, - AssetBaseCfg, - DeformableObject, - DeformableObjectCfg, - RigidObject, - RigidObjectCfg, - RigidObjectCollection, - RigidObjectCollectionCfg, -) -from isaaclab.sensors import ContactSensorCfg, FrameTransformerCfg, SensorBase, SensorBaseCfg -from uwlab.terrains import TerrainImporter, TerrainImporterCfg - -from .interactive_scene_cfg import InteractiveSceneCfg - - -class InteractiveScene: - """A scene that contains entities added to the simulation. - - The interactive scene parses the :class:`InteractiveSceneCfg` class to create the scene. - Based on the specified number of environments, it clones the entities and groups them into different - categories (e.g., articulations, sensors, etc.). - - Cloning can be performed in two ways: - - * For tasks where all environments contain the same assets, a more performant cloning paradigm - can be used to allow for faster environment creation. This is specified by the ``replicate_physics`` flag. - - .. code-block:: python - - scene = InteractiveScene(cfg=InteractiveSceneCfg(replicate_physics=True)) - - * For tasks that require having separate assets in the environments, ``replicate_physics`` would have to - be set to False, which will add some costs to the overall startup time. - - .. code-block:: python - - scene = InteractiveScene(cfg=InteractiveSceneCfg(replicate_physics=False)) - - Each entity is registered to scene based on its name in the configuration class. For example, if the user - specifies a robot in the configuration class as follows: - - .. code-block:: python - - from isaaclab.scene import InteractiveSceneCfg - from isaaclab.utils import configclass - - from isaaclab_assets.robots.anymal import ANYMAL_C_CFG - - @configclass - class MySceneCfg(InteractiveSceneCfg): - - robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") - - Then the robot can be accessed from the scene as follows: - - .. code-block:: python - - from isaaclab.scene import InteractiveScene - - # create 128 environments - scene = InteractiveScene(cfg=MySceneCfg(num_envs=128)) - - # access the robot from the scene - robot = scene["robot"] - # access the robot based on its type - robot = scene.articulations["robot"] - - If the :class:`InteractiveSceneCfg` class does not include asset entities, the cloning process - can still be triggered if assets were added to the stage outside of the :class:`InteractiveScene` class: - - .. code-block:: python - - scene = InteractiveScene(cfg=InteractiveSceneCfg(num_envs=128, replicate_physics=True)) - scene.clone_environments() - - .. note:: - It is important to note that the scene only performs common operations on the entities. For example, - resetting the internal buffers, writing the buffers to the simulation and updating the buffers from the - simulation. The scene does not perform any task specific to the entity. For example, it does not apply - actions to the robot or compute observations from the robot. These tasks are handled by different - modules called "managers" in the framework. Please refer to the :mod:`isaaclab.managers` sub-package - for more details. - """ - - def __init__(self, cfg: InteractiveSceneCfg): - """Initializes the scene. - - Args: - cfg: The configuration class for the scene. - """ - # check that the config is valid - cfg.validate() - # store inputs - self.cfg = cfg - # initialize scene elements - self._terrain = None - self._articulations = dict() - self._deformable_objects = dict() - self._rigid_objects = dict() - self._rigid_object_collections = dict() - self._sensors = dict() - self._extras = dict() - # obtain the current stage - self.stage = omni.usd.get_context().get_stage() - # physics scene path - self._physics_scene_path = None - # prepare cloner for environment replication - self.cloner = GridCloner(spacing=self.cfg.env_spacing) - self.cloner.define_base_env(self.env_ns) - self.env_prim_paths = self.cloner.generate_paths(f"{self.env_ns}/env", self.cfg.num_envs) - # create source prim - self.stage.DefinePrim(self.env_prim_paths[0], "Xform") - - # when replicate_physics=False, we assume heterogeneous environments and clone the xforms first. - # this triggers per-object level cloning in the spawner. - if not self.cfg.replicate_physics: - # clone the env xform - env_origins = self.cloner.clone( - source_prim_path=self.env_prim_paths[0], - prim_paths=self.env_prim_paths, - replicate_physics=False, - copy_from_source=True, - enable_env_ids=self.cfg.filter_collisions, # this won't do anything because we are not replicating physics - ) - self._default_env_origins = torch.tensor(env_origins, device=self.device, dtype=torch.float32) - else: - # otherwise, environment origins will be initialized during cloning at the end of environment creation - self._default_env_origins = None - - self._global_prim_paths = list() - if self._is_scene_setup_from_cfg(): - # add entities from config - self._add_entities_from_cfg() - # clone environments on a global scope if environment is homogeneous - if self.cfg.replicate_physics: - self.clone_environments(copy_from_source=False) - # replicate physics if we have more than one environment - # this is done to make scene initialization faster at play time - if self.cfg.replicate_physics and self.cfg.num_envs > 1: - self.cloner.replicate_physics( - source_prim_path=self.env_prim_paths[0], - prim_paths=self.env_prim_paths, - base_env_path=self.env_ns, - root_path=self.env_regex_ns.replace(".*", ""), - enable_env_ids=self.cfg.filter_collisions, - ) - - # since env_ids is only applicable when replicating physics, we have to fallback to the previous method - # to filter collisions if replicate_physics is not enabled - if not self.cfg.replicate_physics and self.cfg.filter_collisions: - self.filter_collisions(self._global_prim_paths) - - def clone_environments(self, copy_from_source: bool = False): - """Creates clones of the environment ``/World/envs/env_0``. - - Args: - copy_from_source: (bool): If set to False, clones inherit from /World/envs/env_0 and mirror its changes. - If True, clones are independent copies of the source prim and won't reflect its changes (start-up time - may increase). Defaults to False. - """ - # check if user spawned different assets in individual environments - # this flag will be None if no multi asset is spawned - carb_settings_iface = carb.settings.get_settings() - has_multi_assets = carb_settings_iface.get("/isaaclab/spawn/multi_assets") - if has_multi_assets and self.cfg.replicate_physics: - omni.log.warn( - "Varying assets might have been spawned under different environments." - " However, the replicate physics flag is enabled in the 'InteractiveScene' configuration." - " This may adversely affect PhysX parsing. We recommend disabling this property." - ) - - # clone the environment - env_origins = self.cloner.clone( - source_prim_path=self.env_prim_paths[0], - prim_paths=self.env_prim_paths, - replicate_physics=self.cfg.replicate_physics, - copy_from_source=copy_from_source, - enable_env_ids=self.cfg.filter_collisions, # this automatically filters collisions between environments - ) - - # since env_ids is only applicable when replicating physics, we have to fallback to the previous method - # to filter collisions if replicate_physics is not enabled - if not self.cfg.replicate_physics and self.cfg.filter_collisions: - omni.log.warn( - "Collision filtering can only be automatically enabled when replicate_physics=True." - " Please call scene.filter_collisions(global_prim_paths) to filter collisions across environments." - ) - - # in case of heterogeneous cloning, the env origins is specified at init - if self._default_env_origins is None: - self._default_env_origins = torch.tensor(env_origins, device=self.device, dtype=torch.float32) - - def filter_collisions(self, global_prim_paths: list[str] | None = None): - """Filter environments collisions. - - Disables collisions between the environments in ``/World/envs/env_.*`` and enables collisions with the prims - in global prim paths (e.g. ground plane). - - Args: - global_prim_paths: A list of global prim paths to enable collisions with. - Defaults to None, in which case no global prim paths are considered. - """ - # validate paths in global prim paths - if global_prim_paths is None: - global_prim_paths = [] - else: - # remove duplicates in paths - global_prim_paths = list(set(global_prim_paths)) - - # set global prim paths list if not previously defined - if len(self._global_prim_paths) < 1: - self._global_prim_paths += global_prim_paths - - # filter collisions within each environment instance - self.cloner.filter_collisions( - self.physics_scene_path, - "/World/collisions", - self.env_prim_paths, - global_paths=self._global_prim_paths, - ) - - def __str__(self) -> str: - """Returns a string representation of the scene.""" - msg = f"\n" - msg += f"\tNumber of environments: {self.cfg.num_envs}\n" - msg += f"\tEnvironment spacing : {self.cfg.env_spacing}\n" - msg += f"\tSource prim name : {self.env_prim_paths[0]}\n" - msg += f"\tGlobal prim paths : {self._global_prim_paths}\n" - msg += f"\tReplicate physics : {self.cfg.replicate_physics}" - return msg - - """ - Properties. - """ - - @property - def physics_scene_path(self) -> str: - """The path to the USD Physics Scene.""" - if self._physics_scene_path is None: - for prim in self.stage.Traverse(): - if prim.HasAPI(PhysxSchema.PhysxSceneAPI): - self._physics_scene_path = prim.GetPrimPath().pathString - omni.log.info(f"Physics scene prim path: {self._physics_scene_path}") - break - if self._physics_scene_path is None: - raise RuntimeError("No physics scene found! Please make sure one exists.") - return self._physics_scene_path - - @property - def physics_dt(self) -> float: - """The physics timestep of the scene.""" - return sim_utils.SimulationContext.instance().get_physics_dt() # pyright: ignore [reportOptionalMemberAccess] - - @property - def device(self) -> str: - """The device on which the scene is created.""" - return sim_utils.SimulationContext.instance().device # pyright: ignore [reportOptionalMemberAccess] - - @property - def env_ns(self) -> str: - """The namespace ``/World/envs`` in which all environments created. - - The environments are present w.r.t. this namespace under "env_{N}" prim, - where N is a natural number. - """ - return "/World/envs" - - @property - def env_regex_ns(self) -> str: - """The namespace ``/World/envs/env_.*`` in which all environments created.""" - return f"{self.env_ns}/env_.*" - - @property - def num_envs(self) -> int: - """The number of environments handled by the scene.""" - return self.cfg.num_envs - - @property - def env_origins(self) -> torch.Tensor: - """The origins of the environments in the scene. Shape is (num_envs, 3).""" - if self._terrain is not None: - return self._terrain.env_origins - else: - return self._default_env_origins - - @property - def terrain(self) -> TerrainImporter | None: - """The terrain in the scene. If None, then the scene has no terrain. - - Note: - We treat terrain separate from :attr:`extras` since terrains define environment origins and are - handled differently from other miscellaneous entities. - """ - return self._terrain - - @property - def articulations(self) -> dict[str, Articulation]: - """A dictionary of articulations in the scene.""" - return self._articulations - - @property - def deformable_objects(self) -> dict[str, DeformableObject]: - """A dictionary of deformable objects in the scene.""" - return self._deformable_objects - - @property - def rigid_objects(self) -> dict[str, RigidObject]: - """A dictionary of rigid objects in the scene.""" - return self._rigid_objects - - @property - def rigid_object_collections(self) -> dict[str, RigidObjectCollection]: - """A dictionary of rigid object collections in the scene.""" - return self._rigid_object_collections - - @property - def sensors(self) -> dict[str, SensorBase]: - """A dictionary of the sensors in the scene, such as cameras and contact reporters.""" - return self._sensors - - @property - def extras(self) -> dict[str, XFormPrim]: - """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. - - 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. - - Note: - 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.omniverse.nvidia.com/py/isaacsim/source/isaacsim.core/docs/index.html#isaacsim.core.prims.XFormPrim - - """ - return self._extras - - @property - def state(self) -> dict[str, dict[str, dict[str, torch.Tensor]]]: - """Returns the state of the scene entities. - - Returns: - A dictionary of the state of the scene entities. - """ - return self.get_state(is_relative=False) - - def get_state(self, is_relative: bool = False) -> dict[str, dict[str, dict[str, torch.Tensor]]]: - """Returns the state of the scene entities. - - Args: - is_relative: If set to True, the state is considered relative to the environment origins. - - Returns: - A dictionary of the state of the scene entities. - """ - state = dict() - # articulations - state["articulation"] = dict() - for asset_name, articulation in self._articulations.items(): - asset_state = dict() - asset_state["root_pose"] = articulation.data.root_state_w[:, :7].clone() - if is_relative: - asset_state["root_pose"][:, :3] -= self.env_origins - asset_state["root_velocity"] = articulation.data.root_vel_w.clone() - asset_state["joint_position"] = articulation.data.joint_pos.clone() - asset_state["joint_velocity"] = articulation.data.joint_vel.clone() - state["articulation"][asset_name] = asset_state - # deformable objects - state["deformable_object"] = dict() - for asset_name, deformable_object in self._deformable_objects.items(): - asset_state = dict() - asset_state["nodal_position"] = deformable_object.data.nodal_pos_w.clone() - if is_relative: - asset_state["nodal_position"][:, :3] -= self.env_origins - asset_state["nodal_velocity"] = deformable_object.data.nodal_vel_w.clone() - state["deformable_object"][asset_name] = asset_state - # rigid objects - state["rigid_object"] = dict() - for asset_name, rigid_object in self._rigid_objects.items(): - asset_state = dict() - asset_state["root_pose"] = rigid_object.data.root_state_w[:, :7].clone() - if is_relative: - asset_state["root_pose"][:, :3] -= self.env_origins - asset_state["root_velocity"] = rigid_object.data.root_vel_w.clone() - state["rigid_object"][asset_name] = asset_state - return state - - """ - Operations. - """ - - def reset(self, env_ids: Sequence[int] | None = None): - """Resets the scene entities. - - Args: - env_ids: The indices of the environments to reset. - Defaults to None (all instances). - """ - # -- assets - for articulation in self._articulations.values(): - articulation.reset(env_ids) - for deformable_object in self._deformable_objects.values(): - deformable_object.reset(env_ids) - for rigid_object in self._rigid_objects.values(): - rigid_object.reset(env_ids) - for rigid_object_collection in self._rigid_object_collections.values(): - rigid_object_collection.reset(env_ids) - # -- sensors - for sensor in self._sensors.values(): - sensor.reset(env_ids) - - def reset_to( - self, - state: dict[str, dict[str, dict[str, torch.Tensor]]], - env_ids: Sequence[int] | None = None, - is_relative: bool = False, - ): - """Resets the scene entities to the given state. - - Args: - state: The state to reset the scene entities to. - env_ids: The indices of the environments to reset. - Defaults to None (all instances). - is_relative: If set to True, the state is considered relative to the environment origins. - """ - if env_ids is None: - env_ids = slice(None) - # articulations - for asset_name, articulation in self._articulations.items(): - asset_state = state["articulation"][asset_name] - # root state - root_pose = asset_state["root_pose"].clone() - if is_relative: - root_pose[:, :3] += self.env_origins[env_ids] - root_velocity = asset_state["root_velocity"].clone() - articulation.write_root_pose_to_sim(root_pose, env_ids=env_ids) - articulation.write_root_velocity_to_sim(root_velocity, env_ids=env_ids) - # joint state - joint_position = asset_state["joint_position"].clone() - joint_velocity = asset_state["joint_velocity"].clone() - articulation.write_joint_state_to_sim(joint_position, joint_velocity, env_ids=env_ids) - articulation.set_joint_position_target(joint_position, env_ids=env_ids) - articulation.set_joint_velocity_target(joint_velocity, env_ids=env_ids) - # deformable objects - for asset_name, deformable_object in self._deformable_objects.items(): - asset_state = state["deformable_object"][asset_name] - nodal_position = asset_state["nodal_position"].clone() - if is_relative: - nodal_position[:, :3] += self.env_origins[env_ids] - nodal_velocity = asset_state["nodal_velocity"].clone() - deformable_object.write_nodal_pos_to_sim(nodal_position, env_ids=env_ids) - deformable_object.write_nodal_velocity_to_sim(nodal_velocity, env_ids=env_ids) - # rigid objects - for asset_name, rigid_object in self._rigid_objects.items(): - asset_state = state["rigid_object"][asset_name] - root_pose = asset_state["root_pose"].clone() - if is_relative: - root_pose[:, :3] += self.env_origins[env_ids] - root_velocity = asset_state["root_velocity"].clone() - rigid_object.write_root_pose_to_sim(root_pose, env_ids=env_ids) - rigid_object.write_root_velocity_to_sim(root_velocity, env_ids=env_ids) - self.write_data_to_sim() - - def write_data_to_sim(self): - """Writes the data of the scene entities to the simulation.""" - # -- assets - for articulation in self._articulations.values(): - articulation.write_data_to_sim() - for deformable_object in self._deformable_objects.values(): - deformable_object.write_data_to_sim() - for rigid_object in self._rigid_objects.values(): - rigid_object.write_data_to_sim() - for rigid_object_collection in self._rigid_object_collections.values(): - rigid_object_collection.write_data_to_sim() - - def update(self, dt: float) -> None: - """Update the scene entities. - - Args: - dt: The amount of time passed from last :meth:`update` call. - """ - # -- assets - for articulation in self._articulations.values(): - articulation.update(dt) - for deformable_object in self._deformable_objects.values(): - deformable_object.update(dt) - for rigid_object in self._rigid_objects.values(): - rigid_object.update(dt) - for rigid_object_collection in self._rigid_object_collections.values(): - rigid_object_collection.update(dt) - # -- sensors - for sensor in self._sensors.values(): - sensor.update(dt, force_recompute=not self.cfg.lazy_sensor_update) - - """ - Operations: Iteration. - """ - - def keys(self) -> list[str]: - """Returns the keys of the scene entities. - - Returns: - The keys of the scene entities. - """ - all_keys = ["terrain"] - for asset_family in [ - self._articulations, - self._deformable_objects, - self._rigid_objects, - self._rigid_object_collections, - self._sensors, - self._extras, - ]: - all_keys += list(asset_family.keys()) - return all_keys - - def __getitem__(self, key: str) -> Any: - """Returns the scene entity with the given key. - - Args: - key: The key of the scene entity. - - Returns: - The scene entity. - """ - # check if it is a terrain - if key == "terrain": - return self._terrain - - all_keys = ["terrain"] - # check if it is in other dictionaries - for asset_family in [ - self._articulations, - self._deformable_objects, - self._rigid_objects, - self._rigid_object_collections, - self._sensors, - self._extras, - ]: - out = asset_family.get(key) - # if found, return - if out is not None: - return out - all_keys += list(asset_family.keys()) - # if not found, raise error - raise KeyError(f"Scene entity with key '{key}' not found. Available Entities: '{all_keys}'") - - """ - Internal methods. - """ - - def _is_scene_setup_from_cfg(self): - return any( - not (asset_name in InteractiveSceneCfg.__dataclass_fields__ or asset_cfg is None) - for asset_name, asset_cfg in self.cfg.__dict__.items() - ) - - def _add_entities_from_cfg(self): - """Add scene entities from the config.""" - # store paths that are in global collision filter - self._global_prim_paths = list() - # parse the entire scene config and resolve regex - for asset_name, asset_cfg in self.cfg.__dict__.items(): - # skip keywords - # note: easier than writing a list of keywords: [num_envs, env_spacing, lazy_sensor_update] - if asset_name in InteractiveSceneCfg.__dataclass_fields__ or asset_cfg is None: - continue - # resolve regex - if hasattr(asset_cfg, "prim_path"): - asset_cfg.prim_path = asset_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) - # create asset - if isinstance(asset_cfg, TerrainImporterCfg): - # terrains are special entities since they define environment origins - asset_cfg.num_envs = self.cfg.num_envs - asset_cfg.env_spacing = self.cfg.env_spacing - self._terrain = asset_cfg.class_type(asset_cfg) - elif isinstance(asset_cfg, ArticulationCfg): - self._articulations[asset_name] = asset_cfg.class_type(asset_cfg) - elif isinstance(asset_cfg, DeformableObjectCfg): - self._deformable_objects[asset_name] = asset_cfg.class_type(asset_cfg) - elif isinstance(asset_cfg, RigidObjectCfg): - self._rigid_objects[asset_name] = asset_cfg.class_type(asset_cfg) - elif isinstance(asset_cfg, RigidObjectCollectionCfg): - for rigid_object_cfg in asset_cfg.rigid_objects.values(): - rigid_object_cfg.prim_path = rigid_object_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) - self._rigid_object_collections[asset_name] = asset_cfg.class_type(asset_cfg) - for rigid_object_cfg in asset_cfg.rigid_objects.values(): - if hasattr(rigid_object_cfg, "collision_group") and rigid_object_cfg.collision_group == -1: - asset_paths = sim_utils.find_matching_prim_paths(rigid_object_cfg.prim_path) - self._global_prim_paths += asset_paths - elif isinstance(asset_cfg, SensorBaseCfg): - # Update target frame path(s)' regex name space for FrameTransformer - if isinstance(asset_cfg, FrameTransformerCfg): - updated_target_frames = [] - for target_frame in asset_cfg.target_frames: - target_frame.prim_path = target_frame.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) - updated_target_frames.append(target_frame) - asset_cfg.target_frames = updated_target_frames - elif isinstance(asset_cfg, ContactSensorCfg): - updated_filter_prim_paths_expr = [] - 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 - - self._sensors[asset_name] = asset_cfg.class_type(asset_cfg) - elif isinstance(asset_cfg, AssetBaseCfg): - # manually spawn asset - if asset_cfg.spawn is not None: - asset_cfg.spawn.func( - asset_cfg.prim_path, - asset_cfg.spawn, - translation=asset_cfg.init_state.pos, - orientation=asset_cfg.init_state.rot, - ) - # 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) - else: - raise ValueError(f"Unknown asset config type for {asset_name}: {asset_cfg}") - # store global collision paths - if hasattr(asset_cfg, "collision_group") and asset_cfg.collision_group == -1: - asset_paths = sim_utils.find_matching_prim_paths(asset_cfg.prim_path) - self._global_prim_paths += asset_paths diff --git a/source/uwlab/uwlab/scene/interactive_scene_cfg.py b/source/uwlab/uwlab/scene/interactive_scene_cfg.py deleted file mode 100644 index 439dcb5..0000000 --- a/source/uwlab/uwlab/scene/interactive_scene_cfg.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from dataclasses import MISSING - -from isaaclab.utils.configclass import configclass - - -@configclass -class InteractiveSceneCfg: - """Configuration for the interactive scene. - - The users can inherit from this class to add entities to their scene. This is then parsed by the - :class:`InteractiveScene` class to create the scene. - - .. note:: - The adding of entities to the scene is sensitive to the order of the attributes in the configuration. - Please make sure to add the entities in the order you want them to be added to the scene. - The recommended order of specification is terrain, physics-related assets (articulations and rigid bodies), - sensors and non-physics-related assets (lights). - - For example, to add a robot to the scene, the user can create a configuration class as follows: - - .. code-block:: python - - import isaaclab.sim as sim_utils - from isaaclab.assets import AssetBaseCfg - from isaaclab.scene import InteractiveSceneCfg - from isaaclab.sensors.ray_caster import GridPatternCfg, RayCasterCfg - from isaaclab.utils import configclass - - from isaaclab_assets.robots.anymal import ANYMAL_C_CFG - - @configclass - class MySceneCfg(InteractiveSceneCfg): - - # terrain - flat terrain plane - terrain = TerrainImporterCfg( - prim_path="/World/ground", - terrain_type="plane", - ) - - # articulation - robot 1 - robot_1 = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot_1") - # articulation - robot 2 - robot_2 = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot_2") - robot_2.init_state.pos = (0.0, 1.0, 0.6) - - # sensor - ray caster attached to the base of robot 1 that scans the ground - height_scanner = RayCasterCfg( - prim_path="{ENV_REGEX_NS}/Robot_1/base", - offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), - attach_yaw_only=True, - pattern_cfg=GridPatternCfg(resolution=0.1, size=[1.6, 1.0]), - debug_vis=True, - mesh_prim_paths=["/World/ground"], - ) - - # extras - light - light = AssetBaseCfg( - prim_path="/World/light", - spawn=sim_utils.DistantLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)), - init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, 500.0)), - ) - - """ - - num_envs: int = MISSING - """Number of environment instances handled by the scene.""" - - env_spacing: float = MISSING - """Spacing between environments. - - This is the default distance between environment origins in the scene. Used only when the - number of environments is greater than one. - """ - - lazy_sensor_update: bool = True - """Whether to update sensors only when they are accessed. Default is True. - - If true, the sensor data is only updated when their attribute ``data`` is accessed. Otherwise, the sensor - data is updated every time sensors are updated. - """ - - replicate_physics: bool = True - """Enable/disable replication of physics schemas when using the Cloner APIs. Default is True. - - If True, the simulation will have the same asset instances (USD prims) in all the cloned environments. - Internally, this ensures optimization in setting up the scene and parsing it via the physics stage parser. - - If False, the simulation allows having separate asset instances (USD prims) in each environment. - This flexibility comes at a cost of slowdowns in setting up and parsing the scene. - - .. note:: - Optimized parsing of certain prim types (such as deformable objects) is not currently supported - by the physics engine. In these cases, this flag needs to be set to False. - """ - - filter_collisions: bool = True - """Enable/disable collision filtering between cloned environments. Default is True. - - If True, collisions will not occur between cloned environments. - - If False, the simulation will generate collisions between environments. - - .. note:: - Collisions can only be filtered automatically in direct workflows when physics replication is enabled. - If ``replicated_physics=False`` and collision filtering is desired, make sure to call ``scene.filter_collisions()``. - """ diff --git a/source/uwlab/uwlab/scene/scene_context.py b/source/uwlab/uwlab/scene/scene_context.py index d019b1c..c5046f9 100644 --- a/source/uwlab/uwlab/scene/scene_context.py +++ b/source/uwlab/uwlab/scene/scene_context.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any from isaaclab.sensors import SensorBase, SensorBaseCfg + from uwlab.assets import ArticulationCfg, UniversalArticulation if TYPE_CHECKING: diff --git a/source/uwlab/uwlab/scene/scene_context_cfg.py b/source/uwlab/uwlab/scene/scene_context_cfg.py index f0dee4b..1920253 100644 --- a/source/uwlab/uwlab/scene/scene_context_cfg.py +++ b/source/uwlab/uwlab/scene/scene_context_cfg.py @@ -1,9 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from typing import Callable +from collections.abc import Callable from isaaclab.utils.configclass import configclass diff --git a/source/uwlab/uwlab/sim/converters/__init__.py b/source/uwlab/uwlab/sim/converters/__init__.py index 9f012a3..2dc57b1 100644 --- a/source/uwlab/uwlab/sim/converters/__init__.py +++ b/source/uwlab/uwlab/sim/converters/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/sim/converters/common_material_property_cfg.py b/source/uwlab/uwlab/sim/converters/common_material_property_cfg.py index c4f07dd..84a6ea9 100644 --- a/source/uwlab/uwlab/sim/converters/common_material_property_cfg.py +++ b/source/uwlab/uwlab/sim/converters/common_material_property_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/sim/converters/mesh_converter.py b/source/uwlab/uwlab/sim/converters/mesh_converter.py index 6da9c0e..9766cdf 100644 --- a/source/uwlab/uwlab/sim/converters/mesh_converter.py +++ b/source/uwlab/uwlab/sim/converters/mesh_converter.py @@ -1,27 +1,19 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import asyncio import os -from typing import List import isaacsim.core.utils.prims as prim_utils import omni import omni.kit.commands -import omni.usd -from isaacsim.coreutils.extensions import enable_extension -from pxr import Sdf, Usd, UsdGeom, UsdPhysics, UsdShade, UsdUtils - from isaaclab.sim.converters.asset_converter_base import AssetConverterBase from isaaclab.sim.schemas import schemas from isaaclab.sim.utils import clone, export_prim_to_file, get_all_matching_child_prims, safe_set_attribute_on_usd_prim +from isaacsim.coreutils.extensions import enable_extension +from pxr import Sdf, Usd, UsdGeom, UsdPhysics, UsdShade, UsdUtils from .mesh_converter_cfg import MeshConverterCfg @@ -64,7 +56,7 @@ def apply_collision_properties(stage: Usd.Stage, prim_path: str, cfg: MeshConver visibility_attr.Set("invisible", time=Usd.TimeCode.Default()) -def remove_all_other_prims(stage: Usd.Stage, keep_prefix_paths: List[str]) -> None: +def remove_all_other_prims(stage: Usd.Stage, keep_prefix_paths: list[str]) -> None: """Removes all prims except /World and /World/. Args: diff --git a/source/uwlab/uwlab/sim/converters/mesh_converter_cfg.py b/source/uwlab/uwlab/sim/converters/mesh_converter_cfg.py index 3073593..f1aa9ab 100644 --- a/source/uwlab/uwlab/sim/converters/mesh_converter_cfg.py +++ b/source/uwlab/uwlab/sim/converters/mesh_converter_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/sim/spawners/materials/__init__.py b/source/uwlab/uwlab/sim/spawners/materials/__init__.py index 591d8e6..126f860 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/__init__.py +++ b/source/uwlab/uwlab/sim/spawners/materials/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/sim/spawners/materials/common_materials_cfg.py b/source/uwlab/uwlab/sim/spawners/materials/common_materials_cfg.py index ae77703..5ea1054 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/common_materials_cfg.py +++ b/source/uwlab/uwlab/sim/spawners/materials/common_materials_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py b/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py index e7f4778..d248f17 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py +++ b/source/uwlab/uwlab/sim/spawners/materials/physics_materials.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -13,9 +8,8 @@ from typing import TYPE_CHECKING import isaacsim.core.utils.prims as prim_utils -from pxr import PhysxSchema, Usd, UsdPhysics, UsdShade - from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_schema +from pxr import PhysxSchema, Usd, UsdPhysics, UsdShade if TYPE_CHECKING: from . import physics_materials_cfg diff --git a/source/uwlab/uwlab/sim/spawners/materials/physics_materials_cfg.py b/source/uwlab/uwlab/sim/spawners/materials/physics_materials_cfg.py index 2a908b2..bed3c0a 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/physics_materials_cfg.py +++ b/source/uwlab/uwlab/sim/spawners/materials/physics_materials_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py b/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py index c7d2b97..04df498 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py +++ b/source/uwlab/uwlab/sim/spawners/materials/visual_materials.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -14,10 +9,9 @@ import isaacsim.core.utils.prims as prim_utils import omni.kit.commands -from pxr import Gf, Sdf, Usd, UsdShade - from isaaclab.sim.utils import clone, safe_set_attribute_on_usd_prim from isaaclab.utils.assets import NVIDIA_NUCLEUS_DIR +from pxr import Gf, Sdf, Usd, UsdShade if TYPE_CHECKING: from . import visual_materials_cfg diff --git a/source/uwlab/uwlab/sim/spawners/materials/visual_materials_cfg.py b/source/uwlab/uwlab/sim/spawners/materials/visual_materials_cfg.py index dbba910..e0249bf 100644 --- a/source/uwlab/uwlab/sim/spawners/materials/visual_materials_cfg.py +++ b/source/uwlab/uwlab/sim/spawners/materials/visual_materials_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/terrains/__init__.py b/source/uwlab/uwlab/terrains/__init__.py index 9f4ebd6..72254e2 100644 --- a/source/uwlab/uwlab/terrains/__init__.py +++ b/source/uwlab/uwlab/terrains/__init__.py @@ -1,11 +1,7 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from .height_field import * # noqa: F401, F403 -from .terrain_generator import TerrainGenerator -from .terrain_generator_cfg import PatchSamplingCfg, SubTerrainBaseCfg, TerrainGeneratorCfg -from .terrain_importer import TerrainImporter -from .terrain_importer_cfg import TerrainImporterCfg from .trimesh import * # noqa: F401, F403 diff --git a/source/uwlab/uwlab/terrains/config/rough.py b/source/uwlab/uwlab/terrains/config/rough.py index 6230d90..0bcc8a6 100644 --- a/source/uwlab/uwlab/terrains/config/rough.py +++ b/source/uwlab/uwlab/terrains/config/rough.py @@ -1,18 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause """Configuration for custom terrains.""" -import uwlab.terrains as terrain_gen +from isaaclab.terrains import TerrainGeneratorCfg -from ..terrain_generator_cfg import TerrainGeneratorCfg +import uwlab.terrains as terrain_gen ROUGH_TERRAINS_CFG = TerrainGeneratorCfg( size=(8.0, 8.0), diff --git a/source/uwlab/uwlab/terrains/config/terrain_gen.py b/source/uwlab/uwlab/terrains/config/terrain_gen.py deleted file mode 100644 index 424cd0c..0000000 --- a/source/uwlab/uwlab/terrains/config/terrain_gen.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from typing import Dict - -import isaaclab.terrains as terrain_gen -import uwlab.terrains as uw_terrain_gen -from isaaclab.terrains.terrain_generator_cfg import SubTerrainBaseCfg - -TERRAIN_GEN_SUB_TERRAINS: Dict[str, SubTerrainBaseCfg] = { - "perlin": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="perlin", - include_overhang=False, - ), - "ramp_perlin": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.50, - levels=3, - task_descriptor="ramp_perlin", - include_overhang=False, - ), - "wall": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="wall", - include_overhang=False, - ), - "stair_ramp": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="stair_ramp", - include_overhang=False, - ), - "stair_platform": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="stair_platform", - include_overhang=False, - ), - "ramp": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="ramp", - include_overhang=False, - ), - "stair_platform_wall": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="stair_platform_wall", - include_overhang=False, - ), - "perlin_wall": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="perlin_wall", - include_overhang=False, - ), - "box": uw_terrain_gen.CachedTerrainGenCfg( - proportion=1.00, - height=0.25, - levels=3, - task_descriptor="box", - include_overhang=False, - ), - "pyramid_stairs": terrain_gen.MeshPyramidStairsTerrainCfg( - proportion=1.00, - step_height_range=(0.05, 0.07), - step_width=0.3, - platform_width=3.0, - border_width=1.0, - holes=False, - ), - "pyramid_stairs_inv": terrain_gen.MeshInvertedPyramidStairsTerrainCfg( - proportion=1.00, - step_height_range=(0.05, 0.07), - step_width=0.3, - platform_width=3.0, - border_width=1.0, - holes=False, - ), - "boxes": terrain_gen.MeshRandomGridTerrainCfg( - proportion=1.00, grid_width=0.45, grid_height_range=(0.45, 0.57), platform_width=2.0 - ), - "random_rough": terrain_gen.HfRandomUniformTerrainCfg( - proportion=1.00, noise_range=(0.02, 0.04), noise_step=0.02, border_width=0.25 - ), - "hf_pyramid_slope": terrain_gen.HfPyramidSlopedTerrainCfg( - proportion=1.00, slope_range=(0.02, 0.04), platform_width=2.0, border_width=0.25 - ), - "random_grid": terrain_gen.MeshRandomGridTerrainCfg( - proportion=1.00, - platform_width=1.5, - grid_width=0.75, - grid_height_range=(0.025, 0.045), - holes=False, - ), - "discrete_obstacle": terrain_gen.HfDiscreteObstaclesTerrainCfg( - proportion=1.00, - size=(8.0, 8.0), - horizontal_scale=0.1, - vertical_scale=0.005, - border_width=0.0, - num_obstacles=100, - obstacle_height_mode="choice", - obstacle_width_range=(0.25, 0.75), - obstacle_height_range=(1.0, 2.0), - platform_width=1.5, - ), -} diff --git a/source/uwlab/uwlab/terrains/height_field/__init__.py b/source/uwlab/uwlab/terrains/height_field/__init__.py index 2e0f8d2..a8a4b85 100644 --- a/source/uwlab/uwlab/terrains/height_field/__init__.py +++ b/source/uwlab/uwlab/terrains/height_field/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/terrains/height_field/hf_terrains.py b/source/uwlab/uwlab/terrains/height_field/hf_terrains.py index 6bf2a6a..edf93e8 100644 --- a/source/uwlab/uwlab/terrains/height_field/hf_terrains.py +++ b/source/uwlab/uwlab/terrains/height_field/hf_terrains.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/terrains/height_field/hf_terrains_cfg.py b/source/uwlab/uwlab/terrains/height_field/hf_terrains_cfg.py index d29fd04..b23adbf 100644 --- a/source/uwlab/uwlab/terrains/height_field/hf_terrains_cfg.py +++ b/source/uwlab/uwlab/terrains/height_field/hf_terrains_cfg.py @@ -1,18 +1,13 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from dataclasses import MISSING +from isaaclab.terrains.terrain_generator_cfg import SubTerrainBaseCfg from isaaclab.utils import configclass -from ..terrain_generator_cfg import SubTerrainBaseCfg from . import hf_terrains diff --git a/source/uwlab/uwlab/terrains/terrain_generator.py b/source/uwlab/uwlab/terrains/terrain_generator.py deleted file mode 100644 index 6828ae3..0000000 --- a/source/uwlab/uwlab/terrains/terrain_generator.py +++ /dev/null @@ -1,391 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import numpy as np -import os -import torch -import trimesh -from typing import TYPE_CHECKING - -import omni.log - -from isaaclab.terrains.height_field import HfTerrainBaseCfg -from isaaclab.terrains.trimesh.utils import make_border -from isaaclab.terrains.utils import color_meshes_by_height -from isaaclab.utils.dict import dict_to_md5_hash -from isaaclab.utils.io import dump_yaml -from isaaclab.utils.timer import Timer -from isaaclab.utils.warp import convert_to_warp_mesh - -if TYPE_CHECKING: - from .terrain_generator_cfg import PatchSamplingCfg, SubTerrainBaseCfg, TerrainGeneratorCfg - - -class TerrainGenerator: - r"""Terrain generator to handle different terrain generation functions. - - The terrains are represented as meshes. These are obtained either from height fields or by using the - `trimesh `__ library. The height field representation is more - flexible, but it is less computationally and memory efficient than the trimesh representation. - - All terrain generation functions take in the argument :obj:`difficulty` which determines the complexity - of the terrain. The difficulty is a number between 0 and 1, where 0 is the easiest and 1 is the hardest. - In most cases, the difficulty is used for linear interpolation between different terrain parameters. - For example, in a pyramid stairs terrain the step height is interpolated between the specified minimum - and maximum step height. - - Each sub-terrain has a corresponding configuration class that can be used to specify the parameters - of the terrain. The configuration classes are inherited from the :class:`SubTerrainBaseCfg` class - which contains the common parameters for all terrains. - - If a curriculum is used, the terrains are generated based on their difficulty parameter. - The difficulty is varied linearly over the number of rows (i.e. along x) with a small random value - added to the difficulty to ensure that the columns with the same sub-terrain type are not exactly - the same. The difficulty parameter for a sub-terrain at a given row is calculated as: - - .. math:: - - \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: - - .. 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 - 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. - - If the flag :attr:`~TerrainGeneratorCfg.use_cache` is set to True, the terrains are cached based on their - sub-terrain configurations. This means that if the same sub-terrain configuration is used - multiple times, the terrain is only generated once and then reused. This is useful when - generating complex sub-terrains that take a long time to generate. - - .. attention:: - - The terrain generation has its own seed parameter. This is set using the :attr:`TerrainGeneratorCfg.seed` - parameter. If the seed is not set and the caching is disabled, the terrain generation may not be - completely reproducible. - - """ - - terrain_mesh: trimesh.Trimesh - """A single trimesh.Trimesh object for all the generated sub-terrains.""" - terrain_meshes: list[trimesh.Trimesh] - """List of trimesh.Trimesh objects for all the generated sub-terrains.""" - terrain_origins: np.ndarray - """The origin of each sub-terrain. Shape is (num_rows, num_cols, 3).""" - flat_patches: dict[str, torch.Tensor] - """A dictionary of sampled valid (flat) patches for each sub-terrain. - - The dictionary keys are the names of the flat patch sampling configurations. This maps to a - tensor containing the flat patches for each sub-terrain. The shape of the tensor is - (num_rows, num_cols, num_patches, 3). - - For instance, the key "root_spawn" maps to a tensor containing the flat patches for spawning an asset. - Similarly, the key "target_spawn" maps to a tensor containing the flat patches for setting targets. - """ - - def __init__(self, cfg: TerrainGeneratorCfg, device: str = "cpu"): - """Initialize the terrain generator. - - Args: - cfg: Configuration for the terrain generator. - device: The device to use for the flat patches tensor. - """ - # check inputs - if len(cfg.sub_terrains) == 0: - raise ValueError("No sub-terrains specified! Please add at least one sub-terrain.") - # store inputs - self.cfg = cfg - self.device = device - - # set common values to all sub-terrains config - for sub_cfg in self.cfg.sub_terrains.values(): - # size of all terrains - sub_cfg.size = self.cfg.size - # params for height field terrains - if isinstance(sub_cfg, HfTerrainBaseCfg): - sub_cfg.horizontal_scale = self.cfg.horizontal_scale - sub_cfg.vertical_scale = self.cfg.vertical_scale - sub_cfg.slope_threshold = self.cfg.slope_threshold - - # 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( - "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." - ) - - # if the seed is not set, we assume there is a global seed set and use that. - # this ensures that the terrain is reproducible if the seed is set at the beginning of the program. - if self.cfg.seed is not None: - seed = self.cfg.seed - else: - seed = np.random.get_state()[1][0] - # set the seed for reproducibility - # note: we create a new random number generator to avoid affecting the global state - # in the other places where random numbers are used. - self.np_rng = np.random.default_rng(seed) - - # buffer for storing valid patches - self.flat_patches = {} - # create a list of all sub-terrains - self.terrain_meshes = list() - self.terrain_origins = np.zeros((self.cfg.num_rows, self.cfg.num_cols, 3)) - - # parse configuration and add sub-terrains - # create terrains based on curriculum or randomly - if self.cfg.curriculum: - with Timer("[INFO] Generating terrains based on curriculum took"): - self._generate_curriculum_terrains() - else: - with Timer("[INFO] Generating terrains randomly took"): - self._generate_random_terrains() - # add a border around the terrains - self._add_terrain_border() - # combine all the sub-terrains into a single mesh - self.terrain_mesh = trimesh.util.concatenate(self.terrain_meshes) - - # color the terrain mesh - if self.cfg.color_scheme == "height": - self.terrain_mesh = color_meshes_by_height(self.terrain_mesh) - elif self.cfg.color_scheme == "random": - self.terrain_mesh.visual.vertex_colors = self.np_rng.choice( - range(256), size=(len(self.terrain_mesh.vertices), 4) - ) - elif self.cfg.color_scheme == "none": - pass - else: - raise ValueError(f"Invalid color scheme: {self.cfg.color_scheme}.") - - # offset the entire terrain and origins so that it is centered - # -- terrain mesh - transform = np.eye(4) - transform[:2, -1] = -self.cfg.size[0] * self.cfg.num_rows * 0.5, -self.cfg.size[1] * self.cfg.num_cols * 0.5 - self.terrain_mesh.apply_transform(transform) - # -- terrain origins - self.terrain_origins += transform[:3, -1] - # -- valid patches - terrain_origins_torch = torch.tensor(self.terrain_origins, dtype=torch.float, device=self.device).unsqueeze(2) - for name, value in self.flat_patches.items(): - self.flat_patches[name] = value + terrain_origins_torch - - def __str__(self): - """Return a string representation of the terrain generator.""" - msg = "Terrain Generator:" - msg += f"\n\tSeed: {self.cfg.seed}" - msg += f"\n\tNumber of rows: {self.cfg.num_rows}" - msg += f"\n\tNumber of columns: {self.cfg.num_cols}" - msg += f"\n\tSub-terrain size: {self.cfg.size}" - msg += f"\n\tSub-terrain types: {list(self.cfg.sub_terrains.keys())}" - msg += f"\n\tCurriculum: {self.cfg.curriculum}" - msg += f"\n\tDifficulty range: {self.cfg.difficulty_range}" - msg += f"\n\tColor scheme: {self.cfg.color_scheme}" - msg += f"\n\tUse cache: {self.cfg.use_cache}" - if self.cfg.use_cache: - msg += f"\n\tCache directory: {self.cfg.cache_dir}" - - return msg - - """ - Terrain generator functions. - """ - - def _generate_random_terrains(self): - """Add terrains based on randomly sampled difficulty parameter.""" - # normalize the proportions of the sub-terrains - proportions = np.array([sub_cfg.proportion for sub_cfg in self.cfg.sub_terrains.values()]) - proportions /= np.sum(proportions) - # create a list of all terrain configs - sub_terrains_cfgs = list(self.cfg.sub_terrains.values()) - - # randomly sample sub-terrains - for index in range(self.cfg.num_rows * self.cfg.num_cols): - # coordinate index of the sub-terrain - (sub_row, sub_col) = np.unravel_index(index, (self.cfg.num_rows, self.cfg.num_cols)) - # randomly sample terrain index - sub_index = self.np_rng.choice(len(proportions), p=proportions) - # randomly sample difficulty parameter - difficulty = self.np_rng.uniform(*self.cfg.difficulty_range) - # generate terrain - mesh, origin = self._get_terrain_mesh(difficulty, sub_terrains_cfgs[sub_index]) - # add to sub-terrains - self._add_sub_terrain(mesh, origin, sub_row, sub_col, sub_terrains_cfgs[sub_index]) - - def _generate_curriculum_terrains(self): - """Add terrains based on the difficulty parameter.""" - # normalize the proportions of the sub-terrains - proportions = np.array([sub_cfg.proportion for sub_cfg in self.cfg.sub_terrains.values()]) - proportions /= np.sum(proportions) - - # find the sub-terrain index for each column - # we generate the terrains based on their proportion (not randomly sampled) - sub_indices = [] - for index in range(self.cfg.num_cols): - sub_index = np.min(np.where(index / self.cfg.num_cols + 0.001 < np.cumsum(proportions))[0]) - sub_indices.append(sub_index) - sub_indices = np.array(sub_indices, dtype=np.int32) - # create a list of all terrain configs - sub_terrains_cfgs = list(self.cfg.sub_terrains.values()) - - # curriculum-based sub-terrains - for sub_col in range(self.cfg.num_cols): - for sub_row in range(self.cfg.num_rows): - # vary the difficulty parameter linearly over the number of rows - # note: based on the proportion, multiple columns can have the same sub-terrain type. - # Thus to increase the diversity along the rows, we add a small random value to the difficulty. - # This ensures that the terrains are not exactly the same. For example, if the - # the row index is 2 and the number of rows is 10, the nominal difficulty is 0.2. - # We add a small random value to the difficulty to make it between 0.2 and 0.3. - lower, upper = self.cfg.difficulty_range - difficulty = (sub_row + self.np_rng.uniform()) / self.cfg.num_rows - difficulty = lower + (upper - lower) * difficulty - # generate terrain - mesh, origin = self._get_terrain_mesh(difficulty, sub_terrains_cfgs[sub_indices[sub_col]]) - # add to sub-terrains - self._add_sub_terrain(mesh, origin, sub_row, sub_col, sub_terrains_cfgs[sub_indices[sub_col]]) - - """ - Internal helper functions. - """ - - def _add_terrain_border(self): - """Add a surrounding border over all the sub-terrains into the terrain meshes.""" - # border parameters - border_size = ( - self.cfg.num_rows * self.cfg.size[0] + 2 * self.cfg.border_width, - self.cfg.num_cols * self.cfg.size[1] + 2 * self.cfg.border_width, - ) - inner_size = (self.cfg.num_rows * self.cfg.size[0], self.cfg.num_cols * self.cfg.size[1]) - border_center = ( - self.cfg.num_rows * self.cfg.size[0] / 2, - self.cfg.num_cols * self.cfg.size[1] / 2, - -self.cfg.border_height / 2, - ) - # border mesh - border_meshes = make_border(border_size, inner_size, height=self.cfg.border_height, position=border_center) - border = trimesh.util.concatenate(border_meshes) - # update the faces to have minimal triangles - selector = ~(np.asarray(border.triangles)[:, :, 2] < -0.1).any(1) - border.update_faces(selector) - # add the border to the list of meshes - self.terrain_meshes.append(border) - - def _add_sub_terrain( - self, mesh: trimesh.Trimesh, origin: np.ndarray, row: int, col: int, sub_terrain_cfg: SubTerrainBaseCfg - ): - """Add input sub-terrain to the list of sub-terrains. - - This function adds the input sub-terrain mesh to the list of sub-terrains and updates the origin - of the sub-terrain in the list of origins. It also samples flat patches if specified. - - Args: - mesh: The mesh of the sub-terrain. - origin: The origin of the sub-terrain. - row: The row index of the sub-terrain. - col: The column index of the sub-terrain. - """ - # sample flat patches if specified - if sub_terrain_cfg.patch_sampling is not None: - omni.log.info(f"Sampling flat patches for sub-terrain at (row, col): ({row}, {col})") - # convert the mesh to warp mesh - wp_mesh = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=self.device) - # sample flat patches based on each patch configuration for that sub-terrain - for name, patch_cfg in sub_terrain_cfg.patch_sampling.items(): - patch_cfg: PatchSamplingCfg - # create the flat patches tensor (if not already created) - if name not in self.flat_patches: - self.flat_patches[name] = torch.zeros( - (self.cfg.num_rows, self.cfg.num_cols, patch_cfg.num_patches, 3), device=self.device - ) - # add the flat patches to the tensor - self.flat_patches[name][row, col] = patch_cfg.func( - wp_mesh=wp_mesh, - origin=origin, - cfg=patch_cfg, - ) - - # transform the mesh to the correct position - transform = np.eye(4) - transform[0:2, -1] = (row + 0.5) * self.cfg.size[0], (col + 0.5) * self.cfg.size[1] - mesh.apply_transform(transform) - # add mesh to the list - self.terrain_meshes.append(mesh) - # add origin to the list - self.terrain_origins[row, col] = origin + transform[:3, -1] - - def _get_terrain_mesh(self, difficulty: float, cfg: SubTerrainBaseCfg) -> tuple[trimesh.Trimesh, np.ndarray]: - """Generate a sub-terrain mesh based on the input difficulty parameter. - - If caching is enabled, the sub-terrain is cached and loaded from the cache if it exists. - The cache is stored in the cache directory specified in the configuration. - - .. Note: - This function centers the 2D center of the mesh and its specified origin such that the - 2D center becomes :math:`(0, 0)` instead of :math:`(size[0] / 2, size[1] / 2). - - Args: - difficulty: The difficulty parameter. - cfg: The configuration of the sub-terrain. - - Returns: - The sub-terrain mesh and origin. - """ - # copy the configuration - cfg = cfg.copy() - # add other parameters to the sub-terrain configuration - cfg.difficulty = float(difficulty) - cfg.seed = self.cfg.seed - # generate hash for the sub-terrain - sub_terrain_hash = dict_to_md5_hash(cfg.to_dict()) - # generate the file name - sub_terrain_cache_dir = os.path.join(self.cfg.cache_dir, sub_terrain_hash) - sub_terrain_obj_filename = os.path.join(sub_terrain_cache_dir, "mesh.obj") - sub_terrain_csv_filename = os.path.join(sub_terrain_cache_dir, "origin.csv") - sub_terrain_meta_filename = os.path.join(sub_terrain_cache_dir, "cfg.yaml") - - # check if hash exists - if true, load the mesh and origin and return - if self.cfg.use_cache and os.path.exists(sub_terrain_obj_filename): - # load existing mesh - mesh = trimesh.load_mesh(sub_terrain_obj_filename, process=False) - origin = np.loadtxt(sub_terrain_csv_filename, delimiter=",") - # return the generated mesh - return mesh, origin - - # generate the terrain - meshes, origin = cfg.function(difficulty, cfg) - mesh = trimesh.util.concatenate(meshes) - # offset mesh such that they are in their center - transform = np.eye(4) - transform[0:2, -1] = -cfg.size[0] * 0.5, -cfg.size[1] * 0.5 - mesh.apply_transform(transform) - # change origin to be in the center of the sub-terrain - origin += transform[0:3, -1] - - # if caching is enabled, save the mesh and origin - if self.cfg.use_cache: - # create the cache directory - os.makedirs(sub_terrain_cache_dir, exist_ok=True) - # save the data - mesh.export(sub_terrain_obj_filename) - np.savetxt(sub_terrain_csv_filename, origin, delimiter=",", header="x,y,z") - dump_yaml(sub_terrain_meta_filename, cfg) - # return the generated mesh - return mesh, origin diff --git a/source/uwlab/uwlab/terrains/terrain_generator_cfg.py b/source/uwlab/uwlab/terrains/terrain_generator_cfg.py deleted file mode 100644 index 0b0b181..0000000 --- a/source/uwlab/uwlab/terrains/terrain_generator_cfg.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -""" -Configuration classes defining the different terrains available. Each configuration class must -inherit from ``isaaclab.terrains.terrains_cfg.TerrainConfig`` and define the following attributes: - -- ``name``: Name of the terrain. This is used for the prim name in the USD stage. -- ``function``: Function to generate the terrain. This function must take as input the terrain difficulty - and the configuration parameters and return a `tuple with the `trimesh`` mesh object and terrain origin. -""" - -from __future__ import annotations - -import numpy as np -import trimesh -from collections.abc import Callable -from dataclasses import MISSING -from typing import Literal - -from isaaclab.utils import configclass - -from .terrain_generator import TerrainGenerator -from .utils.patch_sampling_cfg import PatchSamplingCfg - - -@configclass -class SubTerrainBaseCfg: - """Base class for terrain configurations. - - All the sub-terrain configurations must inherit from this class. - - The :attr:`size` attribute is the size of the generated sub-terrain. Based on this, the terrain must - extend from :math:`(0, 0)` to :math:`(size[0], size[1])`. - """ - - function: Callable[[float, SubTerrainBaseCfg], tuple[list[trimesh.Trimesh], np.ndarray]] = MISSING - """Function to generate the terrain. - - This function must take as input the terrain difficulty and the configuration parameters and - return a tuple with a list of ``trimesh`` mesh objects and the terrain origin. - """ - - proportion: float = 1.0 - """Proportion of the terrain to generate. Defaults to 1.0. - - This is used to generate a mix of terrains. The proportion corresponds to the probability of sampling - the particular terrain. For example, if there are two terrains, A and B, with proportions 0.3 and 0.7, - respectively, then the probability of sampling terrain A is 0.3 and the probability of sampling terrain B - is 0.7. - """ - - size: tuple[float, float] = (10.0, 10.0) - """The width (along x) and length (along y) of the terrain (in m). Defaults to (10.0, 10.0). - - In case the :class:`~isaaclab.terrains.TerrainImporterCfg` is used, this parameter gets overridden by - :attr:`isaaclab.scene.TerrainImporterCfg.size` attribute. - """ - - patch_sampling: dict[str, PatchSamplingCfg] | None = None - """Dictionary of configurations for sampling flat patches on the sub-terrain. Defaults to None, - in which case no flat patch sampling is performed. - - The keys correspond to the name of the flat patch sampling configuration and the values are the - corresponding configurations. - """ - - -@configclass -class TerrainGeneratorCfg: - """Configuration for the terrain generator.""" - - class_type: Callable = TerrainGenerator - - seed: int | None = None - """The seed for the random number generator. Defaults to None, in which case the seed from the - current NumPy's random state is used. - - When the seed is set, the random number generator is initialized with the given seed. This ensures - that the generated terrains are deterministic across different runs. If the seed is not set, the - seed from the current NumPy's random state is used. This assumes that the seed is set elsewhere in - the code. - """ - - curriculum: bool = False - """Whether to use the curriculum mode. Defaults to False. - - If True, the terrains are generated based on their difficulty parameter. Otherwise, - they are randomly generated. - """ - - size: tuple[float, float] = MISSING - """The width (along x) and length (along y) of each sub-terrain (in m). - - Note: - This value is passed on to all the sub-terrain configurations. - """ - - border_width: float = 0.0 - """The width of the border around the terrain (in m). Defaults to 0.0.""" - - border_height: float = 1.0 - """The height of the border around the terrain (in m). Defaults to 1.0.""" - - num_rows: int = 1 - """Number of rows of sub-terrains to generate. Defaults to 1.""" - - num_cols: int = 1 - """Number of columns of sub-terrains to generate. Defaults to 1.""" - - color_scheme: Literal["height", "random", "none"] = "none" - """Color scheme to use for the terrain. Defaults to "none". - - The available color schemes are: - - - "height": Color based on the height of the terrain. - - "random": Random color scheme. - - "none": No color scheme. - """ - - horizontal_scale: float = 0.1 - """The discretization of the terrain along the x and y axes (in m). Defaults to 0.1. - - This value is passed on to all the height field sub-terrain configurations. - """ - - vertical_scale: float = 0.005 - """The discretization of the terrain along the z axis (in m). Defaults to 0.005. - - This value is passed on to all the height field sub-terrain configurations. - """ - - slope_threshold: float | None = 0.75 - """The slope threshold above which surfaces are made vertical. Defaults to 0.75. - - If None no correction is applied. - - This value is passed on to all the height field sub-terrain configurations. - """ - - sub_terrains: dict[str, SubTerrainBaseCfg] = MISSING - """Dictionary of sub-terrain configurations. - - The keys correspond to the name of the sub-terrain configuration and the values are the corresponding - configurations. - """ - - difficulty_range: tuple[float, float] = (0.0, 1.0) - """The range of difficulty values for the sub-terrains. Defaults to (0.0, 1.0). - - If curriculum is enabled, the terrains will be generated based on this range in ascending order - of difficulty. Otherwise, the terrains will be generated based on this range in a random order. - """ - - use_cache: bool = False - """Whether to load the sub-terrain from cache if it exists. Defaults to True. - - If enabled, the generated terrains are stored in the cache directory. When generating terrains, the cache - is checked to see if the terrain already exists. If it does, the terrain is loaded from the cache. Otherwise, - the terrain is generated and stored in the cache. Caching can be used to speed up terrain generation. - """ - - cache_dir: str = "/tmp/isaaclab/terrains" - """The directory where the terrain cache is stored. Defaults to "/tmp/isaaclab/terrains".""" diff --git a/source/uwlab/uwlab/terrains/terrain_importer.py b/source/uwlab/uwlab/terrains/terrain_importer.py deleted file mode 100644 index a3c8798..0000000 --- a/source/uwlab/uwlab/terrains/terrain_importer.py +++ /dev/null @@ -1,370 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import numpy as np -import torch -import trimesh -from typing import TYPE_CHECKING - -import warp -from pxr import UsdGeom - -import isaaclab.sim as sim_utils -from isaaclab.markers import VisualizationMarkers -from isaaclab.markers.config import FRAME_MARKER_CFG -from isaaclab.terrains.trimesh.utils import make_plane -from isaaclab.terrains.utils import create_prim_from_mesh -from isaaclab.utils.warp import convert_to_warp_mesh -from uwlab.terrains.terrain_generator import TerrainGenerator - -if TYPE_CHECKING: - from .terrain_importer_cfg import TerrainImporterCfg - - -class TerrainImporter: - r"""A class to handle terrain meshes and import them into the simulator. - - We assume that a terrain mesh comprises of sub-terrains that are arranged in a grid with - rows ``num_rows`` and columns ``num_cols``. The terrain origins are the positions of the sub-terrains - where the robot should be spawned. - - Based on the configuration, the terrain importer handles computing the environment origins from the sub-terrain - origins. In a typical setup, the number of sub-terrains (:math:`num\_rows \times num\_cols`) is smaller than - the number of environments (:math:`num\_envs`). In this case, the environment origins are computed by - sampling the sub-terrain origins. - - If a curriculum is used, it is possible to update the environment origins to terrain origins that correspond - to a harder difficulty. This is done by calling :func:`update_terrain_levels`. The idea comes from game-based - curriculum. For example, in a game, the player starts with easy levels and progresses to harder levels. - """ - - meshes: dict[str, trimesh.Trimesh] - """A dictionary containing the names of the meshes and their keys.""" - warp_meshes: dict[str, warp.Mesh] - """A dictionary containing the names of the warp meshes and their keys.""" - terrain_origins: torch.Tensor | None - """The origins of the sub-terrains in the added terrain mesh. Shape is (num_rows, num_cols, 3). - - If None, then it is assumed no sub-terrains exist. The environment origins are computed in a grid. - """ - env_origins: torch.Tensor - """The origins of the environments. Shape is (num_envs, 3).""" - - def __init__(self, cfg: TerrainImporterCfg): - """Initialize the terrain importer. - - Args: - cfg: The configuration for the terrain importer. - - Raises: - ValueError: If input terrain type is not supported. - ValueError: If terrain type is 'generator' and no configuration provided for ``terrain_generator``. - ValueError: If terrain type is 'usd' and no configuration provided for ``usd_path``. - ValueError: If terrain type is 'usd' or 'plane' and no configuration provided for ``env_spacing``. - """ - # check that the config is valid - cfg.validate() - # store inputs - self.cfg = cfg - self.device = sim_utils.SimulationContext.instance().device # type: ignore - - # create a dict of meshes - self.meshes = dict() - self.warp_meshes = dict() - self.env_origins = None - self.terrain_origins = None - # private variables - self._terrain_flat_patches = dict() - - # auto-import the terrain based on the config - if self.cfg.terrain_type == "generator": - # check config is provided - if self.cfg.terrain_generator is None: - raise ValueError("Input terrain type is 'generator' but no value provided for 'terrain_generator'.") - # generate the terrain - terrain_generator: TerrainGenerator = self.cfg.terrain_generator.class_type( - 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) - # refer to the flat patches - self._terrain_flat_patches = terrain_generator.flat_patches - elif self.cfg.terrain_type == "usd": - # check if config is provided - if self.cfg.usd_path is None: - raise ValueError("Input terrain type is 'usd' but no value provided for 'usd_path'.") - # import the terrain - self.import_usd("terrain", self.cfg.usd_path) - # configure the origins in a grid - self.configure_env_origins() - elif self.cfg.terrain_type == "plane": - # load the plane - self.import_ground_plane("terrain") - # configure the origins in a grid - self.configure_env_origins() - else: - raise ValueError(f"Terrain type '{self.cfg.terrain_type}' not available.") - - # set initial state of debug visualization - self.set_debug_vis(self.cfg.debug_vis) - - """ - Properties. - """ - - @property - def has_debug_vis_implementation(self) -> bool: - """Whether the terrain importer has a debug visualization implemented. - - This always returns True. - """ - return True - - @property - def flat_patches(self) -> dict[str, torch.Tensor]: - """A dictionary containing the sampled valid (flat) patches for the terrain. - - This is only available if the terrain type is 'generator'. For other terrain types, this feature - is not available and the function returns an empty dictionary. - - Please refer to the :attr:`TerrainGenerator.flat_patches` for more information. - """ - return self._terrain_flat_patches - - """ - Operations - Visibility. - """ - - def set_debug_vis(self, debug_vis: bool) -> bool: - """Set the debug visualization of the terrain importer. - - Args: - debug_vis: Whether to visualize the terrain origins. - - Returns: - Whether the debug visualization was successfully set. False if the terrain - importer does not support debug visualization. - - Raises: - RuntimeError: If terrain origins are not configured. - """ - # create a marker if necessary - if debug_vis: - if not hasattr(self, "origin_visualizer"): - self.origin_visualizer = VisualizationMarkers( - cfg=FRAME_MARKER_CFG.replace(prim_path="/Visuals/TerrainOrigin") - ) - if self.terrain_origins is not None: - self.origin_visualizer.visualize(self.terrain_origins.reshape(-1, 3)) - elif self.env_origins is not None: - self.origin_visualizer.visualize(self.env_origins.reshape(-1, 3)) - else: - raise RuntimeError("Terrain origins are not configured.") - # set visibility - self.origin_visualizer.set_visibility(True) - else: - if hasattr(self, "origin_visualizer"): - self.origin_visualizer.set_visibility(False) - # report success - return True - - """ - Operations - Import. - """ - - def import_ground_plane(self, key: str, size: tuple[float, float] = (2.0e6, 2.0e6)): - """Add a plane to the terrain importer. - - Args: - key: The key to store the mesh. - size: The size of the plane. Defaults to (2.0e6, 2.0e6). - - Raises: - ValueError: If a terrain with the same key already exists. - """ - # check if key exists - if key in self.meshes: - raise ValueError(f"Mesh with key {key} already exists. Existing keys: {self.meshes.keys()}.") - # create a plane - mesh = make_plane(size, height=0.0, center_zero=True) - # store the mesh - self.meshes[key] = mesh - # create a warp mesh - device = "cuda" if "cuda" in self.device else "cpu" - self.warp_meshes[key] = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=device) - - # get the mesh - ground_plane_cfg = sim_utils.GroundPlaneCfg(physics_material=self.cfg.physics_material, size=size) - ground_plane_cfg.func(self.cfg.prim_path, ground_plane_cfg) - - def import_mesh(self, key: str, mesh: trimesh.Trimesh): - """Import a mesh into the simulator. - - The mesh is imported into the simulator under the prim path ``cfg.prim_path/{key}``. The created path - contains the mesh as a :class:`pxr.UsdGeom` instance along with visual or physics material prims. - - Args: - key: The key to store the mesh. - mesh: The mesh to import. - - Raises: - ValueError: If a terrain with the same key already exists. - """ - # check if key exists - if key in self.meshes: - raise ValueError(f"Mesh with key {key} already exists. Existing keys: {self.meshes.keys()}.") - # store the mesh - self.meshes[key] = mesh - # create a warp mesh - device = "cuda" if "cuda" in self.device else "cpu" - self.warp_meshes[key] = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=device) - - # get the mesh - mesh = self.meshes[key] - mesh_prim_path = self.cfg.prim_path + f"/{key}" - # import the mesh - create_prim_from_mesh( - mesh_prim_path, - mesh, - visual_material=self.cfg.visual_material, - physics_material=self.cfg.physics_material, - ) - - def import_usd(self, key: str, usd_path: str): - """Import a mesh from a USD file. - - We assume that the USD file contains a single mesh. If the USD file contains multiple meshes, then - the first mesh is used. The function mainly helps in registering the mesh into the warp meshes - and the meshes dictionary. - - Note: - We do not apply any material properties to the mesh. The material properties should - be defined in the USD file. - - Args: - key: The key to store the mesh. - usd_path: The path to the USD file. - - Raises: - ValueError: If a terrain with the same key already exists. - """ - # add mesh to the dict - if key in self.meshes: - raise ValueError(f"Mesh with key {key} already exists. Existing keys: {self.meshes.keys()}.") - # add the prim path - cfg = sim_utils.UsdFileCfg(usd_path=usd_path) - cfg.func(self.cfg.prim_path + f"/{key}", cfg) - - # traverse the prim and get the collision mesh - # THINK: Should the user specify the collision mesh? - mesh_prim = sim_utils.get_first_matching_child_prim( - self.cfg.prim_path + f"/{key}", lambda prim: prim.GetTypeName() == "Mesh" - ) - # check if the mesh is valid - if mesh_prim is None: - raise ValueError(f"Could not find any collision mesh in {usd_path}. Please check asset.") - # cast into UsdGeomMesh - mesh_prim = UsdGeom.Mesh(mesh_prim) - # store the mesh - vertices = np.asarray(mesh_prim.GetPointsAttr().Get()) - faces = np.asarray(mesh_prim.GetFaceVertexIndicesAttr().Get()).reshape(-1, 3) - self.meshes[key] = trimesh.Trimesh(vertices=vertices, faces=faces) - # create a warp mesh - device = "cuda" if "cuda" in self.device else "cpu" - self.warp_meshes[key] = convert_to_warp_mesh(vertices, faces, device=device) - - """ - Operations - Origins. - """ - - def configure_env_origins(self, origins: np.ndarray | None = None): - """Configure the origins of the environments based on the added terrain. - - Args: - origins: The origins of the sub-terrains. Shape is (num_rows, num_cols, 3). - """ - # decide whether to compute origins in a grid or based on curriculum - if origins is not None: - # convert to numpy - if isinstance(origins, np.ndarray): - origins = torch.from_numpy(origins) - # store the origins - self.terrain_origins = origins.to(self.device, dtype=torch.float) - # compute environment origins - self.env_origins = self._compute_env_origins_curriculum(self.cfg.num_envs, self.terrain_origins) - else: - self.terrain_origins = None - # check if env spacing is valid - if self.cfg.env_spacing is None: - raise ValueError("Environment spacing must be specified for configuring grid-like origins.") - # compute environment origins - self.env_origins = self._compute_env_origins_grid(self.cfg.num_envs, self.cfg.env_spacing) - - def update_env_origins(self, env_ids: torch.Tensor, move_up: torch.Tensor, move_down: torch.Tensor): - """Update the environment origins based on the terrain levels.""" - # check if grid-like spawning - if self.terrain_origins is None: - return - # update terrain level for the envs - self.terrain_levels[env_ids] += 1 * move_up - 1 * move_down - # robots that solve the last level are sent to a random one - # the minimum level is zero - self.terrain_levels[env_ids] = torch.where( - self.terrain_levels[env_ids] >= self.max_terrain_level, - torch.randint_like(self.terrain_levels[env_ids], self.max_terrain_level), - torch.clip(self.terrain_levels[env_ids], 0), - ) - # update the env origins - self.env_origins[env_ids] = self.terrain_origins[self.terrain_levels[env_ids], self.terrain_types[env_ids]] - - """ - Internal helpers. - """ - - def _compute_env_origins_curriculum(self, num_envs: int, origins: torch.Tensor) -> torch.Tensor: - """Compute the origins of the environments defined by the sub-terrains origins.""" - # extract number of rows and cols - num_rows, num_cols = origins.shape[:2] - # maximum initial level possible for the terrains - if self.cfg.max_init_terrain_level is None: - max_init_level = num_rows - 1 - else: - max_init_level = min(self.cfg.max_init_terrain_level, num_rows - 1) - # store maximum terrain level possible - self.max_terrain_level = num_rows - # define all terrain levels and types available - self.terrain_levels = torch.randint(0, max_init_level + 1, (num_envs,), device=self.device) - self.terrain_types = torch.div( - torch.arange(num_envs, device=self.device), - (num_envs / num_cols), - rounding_mode="floor", - ).to(torch.long) - # create tensor based on number of environments - env_origins = torch.zeros(num_envs, 3, device=self.device) - env_origins[:] = origins[self.terrain_levels, self.terrain_types] - return env_origins - - def _compute_env_origins_grid(self, num_envs: int, env_spacing: float) -> torch.Tensor: - """Compute the origins of the environments in a grid based on configured spacing.""" - # create tensor based on number of environments - env_origins = torch.zeros(num_envs, 3, device=self.device) - # create a grid of origins - num_rows = np.ceil(num_envs / int(np.sqrt(num_envs))) - num_cols = np.ceil(num_envs / num_rows) - ii, jj = torch.meshgrid( - torch.arange(num_rows, device=self.device), torch.arange(num_cols, device=self.device), indexing="ij" - ) - env_origins[:, 0] = -(ii.flatten()[:num_envs] - (num_rows - 1) / 2) * env_spacing - env_origins[:, 1] = (jj.flatten()[:num_envs] - (num_cols - 1) / 2) * env_spacing - env_origins[:, 2] = 0.0 - return env_origins diff --git a/source/uwlab/uwlab/terrains/terrain_importer_cfg.py b/source/uwlab/uwlab/terrains/terrain_importer_cfg.py deleted file mode 100644 index 1bfe4e0..0000000 --- a/source/uwlab/uwlab/terrains/terrain_importer_cfg.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from dataclasses import MISSING -from typing import TYPE_CHECKING, Literal - -import isaaclab.sim as sim_utils -from isaaclab.utils import configclass - -from .terrain_importer import TerrainImporter - -if TYPE_CHECKING: - from .terrain_generator_cfg import TerrainGeneratorCfg - - -@configclass -class TerrainImporterCfg: - """Configuration for the terrain manager.""" - - class_type: type = TerrainImporter - """The class to use for the terrain importer. - - Defaults to :class:`isaaclab.terrains.terrain_importer.TerrainImporter`. - """ - - collision_group: int = -1 - """The collision group of the terrain. Defaults to -1.""" - - prim_path: str = MISSING - """The absolute path of the USD terrain prim. - - All sub-terrains are imported relative to this prim path. - """ - - num_envs: int = 1 - """The number of environment origins to consider. Defaults to 1. - - In case, the :class:`~isaaclab.scene.InteractiveSceneCfg` is used, this parameter gets overridden by - :attr:`isaaclab.scene.InteractiveSceneCfg.num_envs` attribute. - """ - - terrain_type: Literal["generator", "plane", "usd"] = "generator" - """The type of terrain to generate. Defaults to "generator". - - Available options are "plane", "usd", and "generator". - """ - - terrain_generator: TerrainGeneratorCfg | None = None - """The terrain generator configuration. - - Only used if ``terrain_type`` is set to "generator". - """ - - usd_path: str | None = None - """The path to the USD file containing the terrain. - - Only used if ``terrain_type`` is set to "usd". - """ - - env_spacing: float | None = None - """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"``. - """ - - visual_material: sim_utils.VisualMaterialCfg | None = sim_utils.PreviewSurfaceCfg( - diffuse_color=(0.065, 0.0725, 0.080) - ) - """The visual material of the terrain. Defaults to a dark gray color material. - - The material is created at the path: ``{prim_path}/visualMaterial``. If `None`, then no material is created. - - .. note:: - This parameter is used only when the ``terrain_type`` is ``"generator"``. - """ - - physics_material: sim_utils.RigidBodyMaterialCfg = sim_utils.RigidBodyMaterialCfg() - """The physics material of the terrain. Defaults to a default physics material. - - The material is created at the path: ``{prim_path}/physicsMaterial``. - - .. note:: - This parameter is used only when the ``terrain_type`` is ``"generator"`` or ``"plane"``. - """ - - max_init_terrain_level: int | None = None - """The maximum initial terrain level for defining environment origins. Defaults to None. - - The terrain levels are specified by the number of rows in the grid arrangement of - sub-terrains. If None, then the initial terrain level is set to the maximum - terrain level available (``num_rows - 1``). - - Note: - This parameter is used only when sub-terrain origins are defined. - """ - - debug_vis: bool = False - """Whether to enable visualization of terrain origins for the terrain. Defaults to False.""" diff --git a/source/uwlab/uwlab/terrains/trimesh/__init__.py b/source/uwlab/uwlab/terrains/trimesh/__init__.py index 630ed9c..592b391 100644 --- a/source/uwlab/uwlab/terrains/trimesh/__init__.py +++ b/source/uwlab/uwlab/terrains/trimesh/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains.py b/source/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains.py index 063c123..8f9f271 100644 --- a/source/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains.py +++ b/source/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -813,12 +808,10 @@ def repeated_objects_terrain( meshes_list = list() # compute quantities origin = np.asarray((0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.5 * 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/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains_cfg.py b/source/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains_cfg.py index 0b7b86b..00e3076 100644 --- a/source/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains_cfg.py +++ b/source/uwlab/uwlab/terrains/trimesh/basic_mesh_terrains_cfg.py @@ -1,9 +1,4 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -12,9 +7,9 @@ from typing import Literal import isaaclab.terrains.trimesh.utils as mesh_utils_terrains +from isaaclab.terrains import SubTerrainBaseCfg from isaaclab.utils import configclass -from ..terrain_generator_cfg import SubTerrainBaseCfg from . import basic_mesh_terrains as mesh_terrains """ diff --git a/source/uwlab/uwlab/terrains/trimesh/mesh_terrains.py b/source/uwlab/uwlab/terrains/trimesh/mesh_terrains.py index 51aa90e..4cc14d1 100644 --- a/source/uwlab/uwlab/terrains/trimesh/mesh_terrains.py +++ b/source/uwlab/uwlab/terrains/trimesh/mesh_terrains.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -19,13 +19,12 @@ from typing import TYPE_CHECKING import requests - -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - from isaaclab.terrains.trimesh.mesh_terrains import inverted_pyramid_stairs_terrain, pyramid_stairs_terrain from isaaclab.terrains.trimesh.mesh_terrains_cfg import MeshInvertedPyramidStairsTerrainCfg, MeshPyramidStairsTerrainCfg from isaaclab.terrains.trimesh.utils import make_border, make_plane +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + if TYPE_CHECKING: from . import mesh_terrains_cfg diff --git a/source/uwlab/uwlab/terrains/trimesh/mesh_terrains_cfg.py b/source/uwlab/uwlab/terrains/trimesh/mesh_terrains_cfg.py index 6067a0a..66ae98c 100644 --- a/source/uwlab/uwlab/terrains/trimesh/mesh_terrains_cfg.py +++ b/source/uwlab/uwlab/terrains/trimesh/mesh_terrains_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,10 +6,10 @@ from dataclasses import MISSING from typing import Literal -import uwlab.terrains.trimesh.mesh_terrains as mesh_terrains +from isaaclab.terrains.terrain_generator_cfg import SubTerrainBaseCfg from isaaclab.utils import configclass -from ..terrain_generator_cfg import SubTerrainBaseCfg +import uwlab.terrains.trimesh.mesh_terrains as mesh_terrains """ Different trimesh terrain configurations. diff --git a/source/uwlab/uwlab/terrains/utils/__init__.py b/source/uwlab/uwlab/terrains/utils/__init__.py index beaa54b..db1b73e 100644 --- a/source/uwlab/uwlab/terrains/utils/__init__.py +++ b/source/uwlab/uwlab/terrains/utils/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/terrains/utils/patch_sampling.py b/source/uwlab/uwlab/terrains/utils/patch_sampling.py index 878b037..6381e81 100644 --- a/source/uwlab/uwlab/terrains/utils/patch_sampling.py +++ b/source/uwlab/uwlab/terrains/utils/patch_sampling.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING import warp as wp # Warp (https://github.com/NVIDIA/warp) - from isaaclab.utils.warp import raycast_mesh if TYPE_CHECKING: @@ -97,40 +96,40 @@ def find_piecewise_range_flat_patches( mesh_xmin, mesh_xmax = mesh_pts[:, 0].min(), mesh_pts[:, 0].max() mesh_ymin, mesh_ymax = mesh_pts[:, 1].min(), mesh_pts[:, 1].max() - x_range = [cfg.x_ranges] if isinstance(cfg.x_ranges, tuple) else cfg.x_ranges - y_range = [cfg.y_ranges] if isinstance(cfg.y_ranges, tuple) else cfg.y_ranges - z_range = [cfg.z_ranges] if isinstance(cfg.z_ranges, tuple) else cfg.z_ranges + x_range = [cfg.x_range] if isinstance(cfg.x_range, tuple) else cfg.x_range + y_range = [cfg.y_range] if isinstance(cfg.y_range, tuple) else cfg.y_range + z_range = [cfg.z_range] if isinstance(cfg.z_range, tuple) else cfg.z_range # For x-ranges - x_ranges_clipped = [] + x_range_clipped = [] for low, high in x_range: new_low = max(low + origin[0].item(), mesh_xmin) new_high = min(high + origin[0].item(), mesh_xmax) if new_low < new_high: - x_ranges_clipped.append((new_low, new_high)) + x_range_clipped.append((new_low, new_high)) # For y-ranges - y_ranges_clipped = [] + y_range_clipped = [] for low, high in y_range: new_low = max(low + origin[1].item(), mesh_ymin) new_high = min(high + origin[1].item(), mesh_ymax) if new_low < new_high: - y_ranges_clipped.append((new_low, new_high)) + y_range_clipped.append((new_low, new_high)) # For z-ranges, we won't clip by mesh bounding box (optional), # but we shift them by the origin's Z: - z_ranges_shifted = [] + z_range_shifted = [] for low, high in z_range: new_low = low + origin[2].item() new_high = high + origin[2].item() - z_ranges_shifted.append((new_low, new_high)) + z_range_shifted.append((new_low, new_high)) - if not x_ranges_clipped: + if not x_range_clipped: raise ValueError("No valid x-ranges remain after clipping to bounding box.") - if not y_ranges_clipped: + if not y_range_clipped: raise ValueError("No valid y-ranges remain after clipping to bounding box.") - if not z_ranges_shifted: - raise ValueError("z_ranges cannot be empty.") + if not z_range_shifted: + raise ValueError("z_range cannot be empty.") # --- 2) Create a ring of points around (0, 0) in the XY plane to query patch validity angle = torch.linspace(0, 2 * np.pi, 10, device=device) @@ -152,8 +151,8 @@ def find_piecewise_range_flat_patches( iter_count = 0 while len(points_ids) > 0 and iter_count < cfg.max_iterations: # (A) Sample X and Y from the multiple intervals - pos_x = uniform_sample_multiple_ranges(x_ranges_clipped, len(points_ids), device) - pos_y = uniform_sample_multiple_ranges(y_ranges_clipped, len(points_ids), device) + pos_x = uniform_sample_multiple_ranges(x_range_clipped, len(points_ids), device) + pos_y = uniform_sample_multiple_ranges(y_range_clipped, len(points_ids), device) # Store the new (x, y) flat_patches[points_ids, 0] = pos_x @@ -181,10 +180,10 @@ def find_piecewise_range_flat_patches( # (C) Check validity: # 1) The patch ring must lie entirely within at least one z-range interval - # We'll check each ring point's Z to see if it's within ANY of the z_ranges. + # We'll check each ring point's Z to see if it's within ANY of the z_range. # If the ring fails in all intervals, it's invalid. z_ok_mask = torch.zeros(len(points_ids), dtype=torch.bool, device=device) - for zlow, zhigh in z_ranges_shifted: + for zlow, zhigh in z_range_shifted: in_this_range = (heights >= zlow) & (heights <= zhigh) # We only say "ok" if *all* ring points are within the range # for that interval: diff --git a/source/uwlab/uwlab/terrains/utils/patch_sampling_cfg.py b/source/uwlab/uwlab/terrains/utils/patch_sampling_cfg.py index ae34ff9..7821a65 100644 --- a/source/uwlab/uwlab/terrains/utils/patch_sampling_cfg.py +++ b/source/uwlab/uwlab/terrains/utils/patch_sampling_cfg.py @@ -1,12 +1,12 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable from isaaclab.utils import configclass @@ -23,6 +23,17 @@ class PatchSamplingCfg: num_patches: int = MISSING """Number of patches to sample.""" + patch_radius: float | list[float] = MISSING + """Radius of the patches.""" + + patched = False + + def __post_init__(self): + if not self.patched: + cfg = self.to_dict() + cfg["cfg"] = self.__class__ + setattr(self, "patch_radius", cfg) + @configclass class FlatPatchSamplingCfg(PatchSamplingCfg): @@ -69,13 +80,13 @@ class PieceWiseRangeFlatPatchSamplingCfg(PatchSamplingCfg): cases where the terrain may have holes or obstacles in some areas. """ - x_ranges: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) + x_range: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) """The list of (min, max) intervals for X sampling (in mesh frame).""" - y_ranges: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) + y_range: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) """The list of (min, max) intervals for Y sampling (in mesh frame).""" - z_ranges: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) + z_range: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) """The list of (min, max) intervals for Z filtering (in mesh frame).""" max_height_diff: float = MISSING @@ -92,7 +103,14 @@ class FlatPatchSamplingByRadiusCfg(PatchSamplingCfg): radius_range: tuple[float, float] = MISSING + x_range: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) + """The list of (min, max) intervals for X sampling (in mesh frame).""" + + y_range: list[tuple[float, float]] | tuple[float, float] = (-1e6, 1e6) + """The list of (min, max) intervals for Y sampling (in mesh frame).""" + z_range: tuple[float, float] = (-1e6, 1e6) + """The list of (min, max) intervals for Z filtering (in mesh frame).""" max_height_diff: float = MISSING diff --git a/source/uwlab/uwlab/ui/widgets/__init__.py b/source/uwlab/uwlab/ui/widgets/__init__.py new file mode 100644 index 0000000..1ab5f27 --- /dev/null +++ b/source/uwlab/uwlab/ui/widgets/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from .image_plot import ImagePlot +from .line_plot import LiveLinePlot +from .manager_live_visualizer import ManagerLiveVisualizer +from .ui_visualizer_base import UiVisualizerBase diff --git a/source/uwlab/uwlab/ui/widgets/image_plot.py b/source/uwlab/uwlab/ui/widgets/image_plot.py new file mode 100644 index 0000000..4523482 --- /dev/null +++ b/source/uwlab/uwlab/ui/widgets/image_plot.py @@ -0,0 +1,211 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import logging +import numpy as np +from contextlib import suppress +from matplotlib import cm +from typing import TYPE_CHECKING, Optional + +import carb +import omni + +with suppress(ImportError): + # isaacsim.gui is not available when running in headless mode. + import isaacsim.gui.components.ui_utils + +from .ui_widget_wrapper import UIWidgetWrapper + +if TYPE_CHECKING: + import isaacsim.gui.components + import omni.ui + +# import logger +logger = logging.getLogger(__name__) + + +class ImagePlot(UIWidgetWrapper): + """An image plot widget to display live data. + + It has the following Layout where the mode frame is only useful for depth images: + +-------------------------------------------------------+ + | containing_frame | + |+-----------------------------------------------------+| + | main_plot_frame | + ||+---------------------------------------------------+|| + ||| plot_frames ||| + ||| ||| + ||| ||| + ||| (Image Plot Data) ||| + ||| ||| + ||| ||| + |||+-------------------------------------------------+||| + ||| mode_frame ||| + ||| ||| + ||| [x][Absolute] [x][Grayscaled] [ ][Colorized] ||| + |+-----------------------------------------------------+| + +-------------------------------------------------------+ + + """ + + def __init__( + self, + image: Optional[np.ndarray] = None, + label: str = "", + widget_height: int = 200, + show_min_max: bool = True, + unit: tuple[float, str] = (1, ""), + ): + """Create an XY plot UI Widget with axis scaling, legends, and support for multiple plots. + + Overlapping data is most accurately plotted when centered in the frame with reasonable axis scaling. + Pressing down the mouse gives the x and y values of each function at an x coordinate. + + Args: + 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 + """ + self._show_min_max = show_min_max + self._unit_scale = unit[0] + self._unit_name = unit[1] + + self._curr_mode = "None" + + self._has_built = False + + self._enabled = True + + self._byte_provider = omni.ui.ByteImageProvider() + if image is None: + carb.log_warn("image is NONE") + image = np.ones((480, 640, 3), dtype=np.uint8) * 255 + image[:, :, 0] = 0 + image[:, :240, 1] = 0 + + # if image is channel first, convert to channel last + if image.ndim == 3 and image.shape[0] in [1, 3, 4]: + image = np.moveaxis(image, 0, -1) + + self._aspect_ratio = image.shape[1] / image.shape[0] + self._widget_height = widget_height + self._label = label + self.update_image(image) + + plot_frame = self._create_ui_widget() + + super().__init__(plot_frame) + + def setEnabled(self, enabled: bool): + self._enabled = enabled + + def update_image(self, image: np.ndarray): + if not self._enabled: + return + + # if image is channel first, convert to channel last + if image.ndim == 3 and image.shape[0] in [1, 3, 4]: + image = np.moveaxis(image, 0, -1) + + height, width = image.shape[:2] + + if self._curr_mode == "Normalization": + image = (image - image.min()) / (image.max() - image.min()) + image = (image * 255).astype(np.uint8) + elif self._curr_mode == "Colorization": + if image.ndim == 3 and image.shape[2] == 3: + 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") + if image.ndim == 3 and image.shape[2] == 1: + image = (colormap(image).squeeze(2) * 255).astype(np.uint8) + else: + image = (colormap(image) * 255).astype(np.uint8) + + # convert image to 4-channel RGBA + if image.ndim == 2 or (image.ndim == 3 and image.shape[2] == 1): + image = np.dstack((image, image, image, np.full((height, width, 1), 255, dtype=np.uint8))) + + elif image.ndim == 3 and image.shape[2] == 3: + image = np.dstack((image, np.full((height, width, 1), 255, dtype=np.uint8))) + + self._byte_provider.set_bytes_data(image.flatten().data, [width, height]) + + def update_min_max(self, image: np.ndarray): + if self._show_min_max and hasattr(self, "_min_max_label"): + non_inf = image[np.isfinite(image)].flatten() + if len(non_inf) > 0: + self._min_max_label.text = self._get_unit_description( + np.min(non_inf), np.max(non_inf), np.median(non_inf) + ) + else: + self._min_max_label.text = self._get_unit_description(0, 0) + + def _create_ui_widget(self): + containing_frame = omni.ui.Frame(build_fn=self._build_widget) + return containing_frame + + def _get_unit_description(self, min_value: float, max_value: float, median_value: float = None): + return ( + f"Min: {min_value * self._unit_scale:.2f} {self._unit_name} Max:" + f" {max_value * self._unit_scale:.2f} {self._unit_name}" + + (f" Median: {median_value * self._unit_scale:.2f} {self._unit_name}" if median_value is not None else "") + ) + + def _build_widget(self): + + with omni.ui.VStack(spacing=3): + with omni.ui.HStack(): + # Write the leftmost label for what this plot is + omni.ui.Label( + self._label, + width=isaacsim.gui.components.ui_utils.LABEL_WIDTH, + alignment=omni.ui.Alignment.LEFT_TOP, + ) + with omni.ui.Frame(width=self._aspect_ratio * self._widget_height, height=self._widget_height): + self._base_plot = omni.ui.ImageWithProvider(self._byte_provider) + + if self._show_min_max: + self._min_max_label = omni.ui.Label(self._get_unit_description(0, 0)) + + omni.ui.Spacer(height=8) + self._mode_frame = omni.ui.Frame(build_fn=self._build_mode_frame) + + omni.ui.Spacer(width=5) + self._has_built = True + + def _build_mode_frame(self): + """Build the frame containing the mode selection for the plots. + + This is an internal function to build the frame containing the mode selection for the plots. This function + should only be called from within the build function of a frame. + + The built widget has the following layout: + +-------------------------------------------------------+ + | legends_frame | + ||+---------------------------------------------------+|| + ||| ||| + ||| [x][Series 1] [x][Series 2] [ ][Series 3] ||| + ||| ||| + |||+-------------------------------------------------+||| + |+-----------------------------------------------------+| + +-------------------------------------------------------+ + """ + 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, + ) diff --git a/source/uwlab/uwlab/ui/widgets/line_plot.py b/source/uwlab/uwlab/ui/widgets/line_plot.py new file mode 100644 index 0000000..0f13520 --- /dev/null +++ b/source/uwlab/uwlab/ui/widgets/line_plot.py @@ -0,0 +1,608 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import colorsys +import numpy as np +from contextlib import suppress +from typing import TYPE_CHECKING + +import omni +from isaacsim.core.api.simulation_context import SimulationContext + +with suppress(ImportError): + # isaacsim.gui is not available when running in headless mode. + import isaacsim.gui.components.ui_utils + +from .ui_widget_wrapper import UIWidgetWrapper + +if TYPE_CHECKING: + import isaacsim.gui.components + import omni.ui + + +class LiveLinePlot(UIWidgetWrapper): + """A 2D line plot widget to display live data. + + + This widget is used to display live data in a 2D line plot. It can be used to display multiple series + in the same plot. + + It has the following Layout: + +-------------------------------------------------------+ + | containing_frame | + |+-----------------------------------------------------+| + | main_plot_frame | + ||+---------------------------------------------------+|| + ||| plot_frames + grid lines (Z_stacked) ||| + ||| ||| + ||| ||| + ||| (Live Plot Data) ||| + ||| ||| + ||| ||| + |||+-------------------------------------------------+||| + ||| legends_frame ||| + ||| ||| + ||| [x][Series 1] [x][Series 2] [ ][Series 3] ||| + |||+-------------------------------------------------+||| + ||| limits_frame ||| + ||| ||| + ||| [Y-Limits] [min] [max] [Autoscale] ||| + |||+-------------------------------------------------+||| + ||| filter_frame ||| + ||| ||| + ||| ||| + |+-----------------------------------------------------+| + +-------------------------------------------------------+ + + """ + + def __init__( + self, + y_data: list[list[float]], + y_min: float = -10, + y_max: float = 10, + plot_height: int = 150, + show_legend: bool = True, + legends: list[str] | None = None, + max_datapoints: int = 200, + ): + """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_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. + """ + super().__init__(self._create_ui_widget()) + self.plot_height = plot_height + self.show_legend = show_legend + self._legends = legends if legends is not None else ["Series_" + str(i) for i in range(len(y_data))] + self._y_data = y_data + self._colors = self._get_distinct_hex_colors(len(y_data)) + self._y_min = y_min if y_min is not None else -10 + self._y_max = y_max if y_max is not None else 10 + self._max_data_points = max_datapoints + self._show_legend = show_legend + self._series_visible = [True for _ in range(len(y_data))] + self._plot_frames = [] + self._plots = [] + self._plot_selected_values = [] + self._is_built = False + self._filter_frame = None + self._filter_mode = None + self._last_values = None + self._is_paused = False + + # Gets populated when widget is built + self._main_plot_frame = None + + self._autoscale_model = omni.ui.SimpleBoolModel(True) + + """Properties""" + + @property + def autoscale_mode(self) -> bool: + return self._autoscale_model.as_bool + + @property + def y_data(self) -> list[list[float]]: + """The current data in the plot.""" + return self._y_data + + @property + def y_min(self) -> float: + """The current minimum y value.""" + return self._y_min + + @property + def y_max(self) -> float: + """The current maximum y value.""" + return self._y_max + + @property + def legends(self) -> list[str]: + """The current legend labels.""" + return self._legends + + """ General Functions """ + + def clear(self): + """Clears the plot.""" + self._y_data = [[] for _ in range(len(self._y_data))] + self._last_values = None + + for plt in self._plots: + plt.set_data() + + # self._container_frame.rebuild() + + def add_datapoint(self, y_coords: list[float]): + """Add a data point to the plot. + + The data point is added to the end of the plot. If the number of data points exceeds the maximum number + of data points, the oldest data point is removed. + + ``y_coords`` is assumed to be a list of floats with the same length as the number of series in the plot. + + Args: + y_coords: A list of floats containing the y coordinates of the new data points. + """ + + 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:] + + if self._filter_mode == "Lowpass": + if self._last_values is not None: + alpha = 0.8 + y_coord = self._y_data[idx][-1] * alpha + y_coord * (1 - alpha) + elif self._filter_mode == "Integrate": + if self._last_values is not None: + y_coord = self._y_data[idx][-1] + y_coord + elif self._filter_mode == "Derivative": + if self._last_values is not None: + y_coord = (y_coord - self._last_values[idx]) / SimulationContext.instance().get_rendering_dt() + + self._y_data[idx].append(float(y_coord)) + + if self._main_plot_frame is None: + # Widget not built, not visible + return + + # Check if the widget has been built, i.e. the plot references have been created. + if not self._is_built or self._is_paused: + return + + if len(self._y_data) != len(self._plots): + # Plots gotten out of sync, rebuild the widget + self._main_plot_frame.rebuild() + return + + if self.autoscale_mode: + self._rescale_btn_pressed() + + for idx, plt in enumerate(self._plots): + plt.set_data(*self._y_data[idx]) + + self._last_values = y_coords + # Autoscale the y-axis to the current data + + """ + Internal functions for building the UI. + """ + + def _build_stacked_plots(self, grid: bool = True): + """Builds multiple plots stacked on top of each other to display multiple series. + + This is an internal function to build the plots. It should not be called from outside the class and only + from within the build function of a frame. + + The built widget has the following layout: + +-------------------------------------------------------+ + | main_plot_frame | + ||+---------------------------------------------------+|| + ||| ||| + ||| y_max|*******-------------------*******| ||| + ||| |-------*****-----------**--------| ||| + ||| 0|------------**-----***-----------| ||| + ||| |--------------***----------------| ||| + ||| y_min|---------------------------------| ||| + ||| ||| + |||+-------------------------------------------------+||| + + + Args: + grid: Whether to display grid lines. Defaults to True. + """ + + # Reset lists which are populated in the build function + self._plot_frames = [] + + # Define internal builder function + def _build_single_plot(y_data: list[float], color: int, plot_idx: int): + """Build a single plot. + + This is an internal function to build a single plot with the given data and color. This function + should only be called from within the build function of a frame. + + Args: + y_data: The data to plot. + color: The color of the plot. + """ + plot = omni.ui.Plot( + omni.ui.Type.LINE, + self._y_min, + self._y_max, + *y_data, + height=self.plot_height, + style={"color": color, "background_color": 0x0}, + ) + + if len(self._plots) <= plot_idx: + self._plots.append(plot) + self._plot_selected_values.append(omni.ui.SimpleStringModel("")) + else: + self._plots[plot_idx] = plot + + # Begin building the widget + with omni.ui.HStack(): + # Space to the left to add y-axis labels + omni.ui.Spacer(width=20) + + # Built plots for each time series stacked on top of each other + with omni.ui.ZStack(): + # Background rectangle + omni.ui.Rectangle( + height=self.plot_height, + style={ + "background_color": 0x0, + "border_color": omni.ui.color.white, + "border_width": 0.4, + "margin": 0.0, + }, + ) + + # Draw grid lines and labels + if grid: + # Calculate the number of grid lines to display + # Absolute range of the plot + plot_range = self._y_max - self._y_min + grid_resolution = 10 ** np.floor(np.log10(0.5 * plot_range)) + + plot_range /= grid_resolution + + # Fraction of the plot range occupied by the first and last grid line + first_space = (self._y_max / grid_resolution) - np.floor(self._y_max / grid_resolution) + last_space = np.ceil(self._y_min / grid_resolution) - self._y_min / grid_resolution + + # Number of grid lines to display + n_lines = int(plot_range - first_space - last_space) + + plot_resolution = self.plot_height / plot_range + + with omni.ui.VStack(): + omni.ui.Spacer(height=plot_resolution * first_space) + + # Draw grid lines + with omni.ui.VGrid(row_height=plot_resolution): + for grid_line_idx in range(n_lines): + # Create grid line + with omni.ui.ZStack(): + omni.ui.Line( + style={ + "color": 0xAA8A8777, + "background_color": 0x0, + "border_width": 0.4, + }, + alignment=omni.ui.Alignment.CENTER_TOP, + height=0, + ) + with omni.ui.Placer(offset_x=-20): + omni.ui.Label( + f"{(self._y_max - first_space * grid_resolution - grid_line_idx * grid_resolution):.3f}", + width=8, + height=8, + alignment=omni.ui.Alignment.RIGHT_TOP, + style={ + "color": 0xFFFFFFFF, + "font_size": 8, + }, + ) + + # Create plots for each series + for idx, (data, color) in enumerate(zip(self._y_data, self._colors)): + plot_frame = omni.ui.Frame( + build_fn=lambda y_data=data, plot_idx=idx, color=color: _build_single_plot( + y_data, color, plot_idx + ), + ) + plot_frame.visible = self._series_visible[idx] + self._plot_frames.append(plot_frame) + + # Create an invisible frame on top that will give a helpful tooltip + self._tooltip_frame = omni.ui.Plot( + height=self.plot_height, + style={"color": 0xFFFFFFFF, "background_color": 0x0}, + ) + + self._tooltip_frame.set_mouse_pressed_fn(self._mouse_moved_on_plot) + + # Create top label for the y-axis + with omni.ui.Placer(offset_x=-20, offset_y=-8): + omni.ui.Label( + f"{self._y_max:.3f}", + width=8, + height=2, + alignment=omni.ui.Alignment.LEFT_TOP, + style={"color": 0xFFFFFFFF, "font_size": 8}, + ) + + # Create bottom label for the y-axis + with omni.ui.Placer(offset_x=-20, offset_y=self.plot_height): + omni.ui.Label( + f"{self._y_min:.3f}", + width=8, + height=2, + alignment=omni.ui.Alignment.LEFT_BOTTOM, + style={"color": 0xFFFFFFFF, "font_size": 8}, + ) + + def _mouse_moved_on_plot(self, x, y, *args): + # Show a tooltip with x,y and function values + if len(self._y_data) == 0 or len(self._y_data[0]) == 0: + # There is no data in the plots, so do nothing + return + + for idx, plot in enumerate(self._plots): + x_pos = plot.screen_position_x + width = plot.computed_width + + location_x = (x - x_pos) / width + + data = self._y_data[idx] + n_samples = len(data) + selected_sample = int(location_x * n_samples) + value = data[selected_sample] + # save the value in scientific notation + self._plot_selected_values[idx].set_value(f"{value:.3f}") + + def _build_legends_frame(self): + """Build the frame containing the legend for the plots. + + This is an internal function to build the frame containing the legend for the plots. This function + should only be called from within the build function of a frame. + + The built widget has the following layout: + +-------------------------------------------------------+ + | legends_frame | + ||+---------------------------------------------------+|| + ||| ||| + ||| [x][Series 1] [x][Series 2] [ ][Series 3] ||| + ||| ||| + |||+-------------------------------------------------+||| + |+-----------------------------------------------------+| + +-------------------------------------------------------+ + """ + if not self._show_legend: + return + + with omni.ui.HStack(): + omni.ui.Spacer(width=32) + + # Find the longest legend to determine the width of the frame + max_legend = max([len(legend) for legend in self._legends]) + CHAR_WIDTH = 8 + with omni.ui.VGrid( + row_height=isaacsim.gui.components.ui_utils.LABEL_HEIGHT, + column_width=max_legend * CHAR_WIDTH + 6, + ): + for idx in range(len(self._y_data)): + with omni.ui.HStack(): + model = omni.ui.SimpleBoolModel() + model.set_value(self._series_visible[idx]) + omni.ui.CheckBox(model=model, tooltip="", width=4) + model.add_value_changed_fn(lambda val, idx=idx: self._change_plot_visibility(idx, val.as_bool)) + omni.ui.Spacer(width=2) + with omni.ui.VStack(): + omni.ui.Label( + self._legends[idx], + width=max_legend * CHAR_WIDTH, + alignment=omni.ui.Alignment.LEFT, + style={"color": self._colors[idx], "font_size": 12}, + ) + omni.ui.StringField( + model=self._plot_selected_values[idx], + width=max_legend * CHAR_WIDTH, + alignment=omni.ui.Alignment.LEFT, + style={"color": self._colors[idx], "font_size": 10}, + read_only=True, + ) + + def _build_limits_frame(self): + """Build the frame containing the controls for the y-axis limits. + + This is an internal function to build the frame containing the controls for the y-axis limits. This function + should only be called from within the build function of a frame. + + The built widget has the following layout: + +-------------------------------------------------------+ + | limits_frame | + ||+---------------------------------------------------+|| + ||| ||| + ||| Limits [min] [max] [Re-Sacle] ||| + ||| Autoscale[x] ||| + ||| ------------------------------------------- ||| + |||+-------------------------------------------------+||| + """ + with omni.ui.VStack(): + with omni.ui.HStack(): + omni.ui.Label( + "Limits", + width=isaacsim.gui.components.ui_utils.LABEL_WIDTH, + alignment=omni.ui.Alignment.LEFT_CENTER, + ) + + self.lower_limit_drag = omni.ui.FloatDrag(name="min", enabled=True, alignment=omni.ui.Alignment.CENTER) + y_min_model = self.lower_limit_drag.model + y_min_model.set_value(self._y_min) + y_min_model.add_value_changed_fn(lambda x: self._set_y_min(x.as_float)) + omni.ui.Spacer(width=2) + + self.upper_limit_drag = omni.ui.FloatDrag(name="max", enabled=True, alignment=omni.ui.Alignment.CENTER) + y_max_model = self.upper_limit_drag.model + y_max_model.set_value(self._y_max) + y_max_model.add_value_changed_fn(lambda x: self._set_y_max(x.as_float)) + omni.ui.Spacer(width=2) + + omni.ui.Button( + "Re-Scale", + width=isaacsim.gui.components.ui_utils.BUTTON_WIDTH, + clicked_fn=self._rescale_btn_pressed, + alignment=omni.ui.Alignment.LEFT_CENTER, + style=isaacsim.gui.components.ui_utils.get_style(), + ) + + omni.ui.CheckBox(model=self._autoscale_model, tooltip="", width=4) + + omni.ui.Line( + style={"color": 0x338A8777}, + width=omni.ui.Fraction(1), + alignment=omni.ui.Alignment.CENTER, + ) + + def _build_filter_frame(self): + """Build the frame containing the filter controls. + + This is an internal function to build the frame containing the filter controls. This function + should only be called from within the build function of a frame. + + The built widget has the following layout: + +-------------------------------------------------------+ + | filter_frame | + ||+---------------------------------------------------+|| + ||| ||| + ||| ||| + ||| ||| + |||+-------------------------------------------------+||| + |+-----------------------------------------------------+| + +-------------------------------------------------------+ + """ + with omni.ui.VStack(): + with omni.ui.HStack(): + + def _filter_changed(value): + self.clear() + self._filter_mode = value + + isaacsim.gui.components.ui_utils.dropdown_builder( + label="Filter", + type="dropdown", + items=["None", "Lowpass", "Integrate", "Derivative"], + tooltip="Select a filter", + on_clicked_fn=_filter_changed, + ) + + def _toggle_paused(): + self._is_paused = not self._is_paused + + # Button + omni.ui.Button( + "Play/Pause", + width=isaacsim.gui.components.ui_utils.BUTTON_WIDTH, + clicked_fn=_toggle_paused, + alignment=omni.ui.Alignment.LEFT_CENTER, + style=isaacsim.gui.components.ui_utils.get_style(), + ) + + def _create_ui_widget(self): + """Create the full UI widget.""" + + def _build_widget(): + self._is_built = False + with omni.ui.VStack(): + self._main_plot_frame = omni.ui.Frame(build_fn=self._build_stacked_plots) + omni.ui.Spacer(height=8) + self._legends_frame = omni.ui.Frame(build_fn=self._build_legends_frame) + omni.ui.Spacer(height=8) + self._limits_frame = omni.ui.Frame(build_fn=self._build_limits_frame) + omni.ui.Spacer(height=8) + self._filter_frame = omni.ui.Frame(build_fn=self._build_filter_frame) + self._is_built = True + + containing_frame = omni.ui.Frame(build_fn=_build_widget) + + return containing_frame + + """ UI Actions Listener Functions """ + + def _change_plot_visibility(self, idx: int, visible: bool): + """Change the visibility of a plot at position idx.""" + self._series_visible[idx] = visible + self._plot_frames[idx].visible = visible + # self._main_plot_frame.rebuild() + + def _set_y_min(self, val: float): + """Update the y-axis minimum.""" + self._y_min = val + self.lower_limit_drag.model.set_value(val) + self._main_plot_frame.rebuild() + + def _set_y_max(self, val: float): + """Update the y-axis maximum.""" + self._y_max = val + self.upper_limit_drag.model.set_value(val) + self._main_plot_frame.rebuild() + + def _rescale_btn_pressed(self): + """Autoscale the y-axis to the current data.""" + if any(self._series_visible): + y_min = np.round( + min([min(y) for idx, y in enumerate(self._y_data) if self._series_visible[idx]]), + 4, + ) + y_max = np.round( + max([max(y) for idx, y in enumerate(self._y_data) if self._series_visible[idx]]), + 4, + ) + if y_min == y_max: + y_max += 1e-4 # Make sure axes don't collapse + + self._y_max = y_max + self._y_min = y_min + + if hasattr(self, "lower_limit_drag") and hasattr(self, "upper_limit_drag"): + self.lower_limit_drag.model.set_value(self._y_min) + self.upper_limit_drag.model.set_value(self._y_max) + + self._main_plot_frame.rebuild() + + """ Helper Functions """ + + def _get_distinct_hex_colors(self, num_colors) -> list[int]: + """ + This function returns a list of distinct colors for plotting. + + Args: + num_colors (int): the number of colors to generate + + Returns: + List[int]: a list of distinct colors in hexadecimal format 0xFFBBGGRR + """ + # Generate equally spaced colors in HSV space + rgb_colors = [ + colorsys.hsv_to_rgb(hue / num_colors, 0.75, 1) for hue in np.linspace(0, num_colors - 1, num_colors) + ] + # Convert to 0-255 RGB values + rgb_colors = [[int(c * 255) for c in rgb] for rgb in rgb_colors] + # Convert to 0xFFBBGGRR format + hex_colors = [0xFF * 16**6 + c[2] * 16**4 + c[1] * 16**2 + c[0] for c in rgb_colors] + return hex_colors diff --git a/source/uwlab/uwlab/ui/widgets/manager_live_visualizer.py b/source/uwlab/uwlab/ui/widgets/manager_live_visualizer.py new file mode 100644 index 0000000..eade182 --- /dev/null +++ b/source/uwlab/uwlab/ui/widgets/manager_live_visualizer.py @@ -0,0 +1,299 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import logging +import numpy +import weakref +from dataclasses import MISSING +from typing import TYPE_CHECKING + +import omni.kit.app +from isaaclab.managers import ManagerBase +from isaaclab.utils import configclass +from isaacsim.core.api.simulation_context import SimulationContext + +from .image_plot import ImagePlot +from .line_plot import LiveLinePlot +from .ui_visualizer_base import UiVisualizerBase + +if TYPE_CHECKING: + import omni.ui + +# import logger +logger = logging.getLogger(__name__) + + +@configclass +class ManagerLiveVisualizerCfg: + """Configuration for the :class:`ManagerLiveVisualizer` class.""" + + debug_vis: bool = False + """Flag used to set status of the live visualizers on startup. Defaults to False, which means closed.""" + + manager_name: str = MISSING + """Manager name that corresponds to the manager of interest in the ManagerBasedEnv and ManagerBasedRLEnv""" + + term_names: list[str] | dict[str, list[str]] | None = None + """Specific term names specified in a Manager config that are chosen to be plotted. Defaults to None. + + If None all terms will be plotted. For managers that utilize Groups (i.e. ObservationGroup) use a dictionary of + {group_names: [term_names]}. + """ + + +class ManagerLiveVisualizer(UiVisualizerBase): + """A interface object used to transfer data from a manager to a UI widget. + + This class handles the creation of UI Widgets for selected terms given a :class:`ManagerLiveVisualizerCfg`. + It iterates through the terms of the manager and creates a visualizer for each term. If the term is a single + variable or a multi-variable signal, it creates a :class:`LiveLinePlot`. If the term is an image (2D or RGB), + it creates an :class:`ImagePlot`. The visualizer can be toggled on and off using the + :attr:`ManagerLiveVisualizerCfg.debug_vis` flag in the configuration. + """ + + def __init__(self, manager: ManagerBase, cfg: ManagerLiveVisualizerCfg = ManagerLiveVisualizerCfg()): + """Initialize ManagerLiveVisualizer. + + Args: + manager: The manager with terms to be plotted. The manager must have a :meth:`get_active_iterable_terms` method. + cfg: The configuration file used to select desired manager terms to be plotted. + """ + + self._manager = manager + self.debug_vis = cfg.debug_vis + self._env_idx: int = 0 + self.cfg = cfg + self._viewer_env_idx = 0 + self._vis_frame: omni.ui.Frame + self._vis_window: omni.ui.Window + + # evaluate chosen terms if no terms provided use all available. + self.term_names = [] + + if self.cfg.term_names is not None: + # extract chosen terms + if isinstance(self.cfg.term_names, list): + for term_name in self.cfg.term_names: + if term_name in self._manager.active_terms: + self.term_names.append(term_name) + else: + logger.error( + f"ManagerVisualizer Failure: ManagerTerm ({term_name}) does not exist in" + f" Manager({self.cfg.manager_name})" + ) + + # extract chosen group-terms + elif isinstance(self.cfg.term_names, dict): + # if manager is using groups and terms are saved as a dictionary + if isinstance(self._manager.active_terms, dict): + for group, terms in self.cfg.term_names: + if group in self._manager.active_terms.keys(): + for term_name in terms: + if term_name in self._manager.active_terms[group]: + self.term_names.append(f"{group}-{term_name}") + else: + logger.error( + f"ManagerVisualizer Failure: ManagerTerm ({term_name}) does not exist in" + f" Group({group})" + ) + else: + logger.error( + f"ManagerVisualizer Failure: Group ({group}) does not exist in" + f" Manager({self.cfg.manager_name})" + ) + else: + logger.error( + f"ManagerVisualizer Failure: Manager({self.cfg.manager_name}) does not utilize grouping of" + " terms." + ) + + # + # Implementation checks + # + + @property + def get_vis_frame(self) -> omni.ui.Frame: + """Returns the UI Frame object tied to this visualizer.""" + return self._vis_frame + + @property + def get_vis_window(self) -> omni.ui.Window: + """Returns the UI Window object tied to this visualizer.""" + return self._vis_window + + # + # Setters + # + + def set_debug_vis(self, debug_vis: bool): + """Set the debug visualization external facing function. + + Args: + debug_vis: Whether to enable or disable the debug visualization. + """ + self._set_debug_vis_impl(debug_vis) + + # + # Implementations + # + + def _set_env_selection_impl(self, env_idx: int): + """Update the index of the selected environment to display. + + Args: + env_idx: The index of the selected environment. + """ + if env_idx > 0 and env_idx < self._manager.num_envs: + self._env_idx = env_idx + else: + 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. + + Args: + frame: The debug visualization frame. + """ + self._vis_frame = frame + + def _debug_vis_callback(self, event): + """Callback for the debug visualization event.""" + + if not SimulationContext.instance().is_playing(): + # Visualizers have not been created yet. + return + + # get updated data and update visualization + for (_, term), vis in zip( + self._manager.get_active_iterable_terms(env_idx=self._env_idx), self._term_visualizers + ): + if isinstance(vis, LiveLinePlot): + vis.add_datapoint(term) + elif isinstance(vis, ImagePlot): + vis.update_image(numpy.array(term)) + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set the debug visualization implementation. + + Args: + debug_vis: Whether to enable or disable debug visualization. + """ + + if not hasattr(self, "_vis_frame"): + raise RuntimeError("No frame set for debug visualization.") + + # Clear internal visualizers + self._term_visualizers = [] + self._vis_frame.clear() + + if debug_vis: + # if enabled create a subscriber for the post update event if it doesn't exist + if not hasattr(self, "_debug_vis_handle") or self._debug_vis_handle is None: + app_interface = omni.kit.app.get_app_interface() + self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) + ) + else: + # if disabled remove the subscriber if it exists + if self._debug_vis_handle is not None: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + + self._vis_frame.visible = False + return + + self._vis_frame.visible = True + + with self._vis_frame: + with omni.ui.VStack(): + # Add a plot in a collapsible frame for each term available + for name, term in self._manager.get_active_iterable_terms(env_idx=self._env_idx): + if name in self.term_names or len(self.term_names) == 0: + frame = omni.ui.CollapsableFrame( + name, + collapsed=False, + style={"border_color": 0xFF8A8777, "padding": 4}, + ) + with frame: + # create line plot for single or multi-variable signals + len_term_shape = len(numpy.array(term).shape) + if len_term_shape <= 2: + plot = LiveLinePlot(y_data=[[elem] for elem in term], plot_height=150, show_legend=True) + self._term_visualizers.append(plot) + # create an image plot for 2d and greater data (i.e. mono and rgb images) + elif len_term_shape == 3: + image = ImagePlot(image=numpy.array(term), label=name) + self._term_visualizers.append(image) + else: + logger.warning( + f"ManagerLiveVisualizer: Term ({name}) is not a supported data type for" + " visualization." + ) + frame.collapsed = True + + self._debug_vis = debug_vis + + +@configclass +class DefaultManagerBasedEnvLiveVisCfg: + """Default configuration to use for the ManagerBasedEnv. Each chosen manager assumes all terms will be plotted.""" + + action_live_vis = ManagerLiveVisualizerCfg(manager_name="action_manager") + observation_live_vis = ManagerLiveVisualizerCfg(manager_name="observation_manager") + + +@configclass +class DefaultManagerBasedRLEnvLiveVisCfg(DefaultManagerBasedEnvLiveVisCfg): + """Default configuration to use for the ManagerBasedRLEnv. Each chosen manager assumes all terms will be plotted.""" + + curriculum_live_vis = ManagerLiveVisualizerCfg(manager_name="curriculum_manager") + command_live_vis = ManagerLiveVisualizerCfg(manager_name="command_manager") + reward_live_vis = ManagerLiveVisualizerCfg(manager_name="reward_manager") + termination_live_vis = ManagerLiveVisualizerCfg(manager_name="termination_manager") + + +class EnvLiveVisualizer: + """A class to handle all ManagerLiveVisualizers used in an Environment.""" + + def __init__(self, cfg: object, managers: dict[str, ManagerBase]): + """Initialize the EnvLiveVisualizer. + + Args: + cfg: The configuration file containing terms of ManagerLiveVisualizers. + managers: A dictionary of labeled managers. i.e. {"manager_name",manager}. + """ + self.cfg = cfg + self.managers = managers + self._prepare_terms() + + def _prepare_terms(self): + self._manager_visualizers: dict[str, ManagerLiveVisualizer] = dict() + + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + + for term_name, term_cfg in cfg_items: + # check if term config is None + if term_cfg is None: + continue + # check if term config is viable + if isinstance(term_cfg, ManagerLiveVisualizerCfg): + # find appropriate manager name + manager = self.managers[term_cfg.manager_name] + self._manager_visualizers[term_cfg.manager_name] = ManagerLiveVisualizer(manager=manager, cfg=term_cfg) + else: + raise TypeError( + f"Provided EnvLiveVisualizer term: '{term_name}' is not of type ManagerLiveVisualizerCfg" + ) + + @property + def manager_visualizers(self) -> dict[str, ManagerLiveVisualizer]: + """A dictionary of labeled ManagerLiveVisualizers associated manager name as key.""" + return self._manager_visualizers diff --git a/source/uwlab/uwlab/ui/widgets/ui_visualizer_base.py b/source/uwlab/uwlab/ui/widgets/ui_visualizer_base.py new file mode 100644 index 0000000..28a9988 --- /dev/null +++ b/source/uwlab/uwlab/ui/widgets/ui_visualizer_base.py @@ -0,0 +1,148 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import inspect +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import omni.ui + + +class UiVisualizerBase: + """Base Class for components that support debug visualizations that requires access to some UI elements. + + This class provides a set of functions that can be used to assign ui interfaces. + + The following functions are provided: + + * :func:`set_debug_vis`: Assigns a debug visualization interface. This function is called by the main UI + when the checkbox for debug visualization is toggled. + * :func:`set_vis_frame`: Assigns a small frame within the isaac lab tab that can be used to visualize debug + information. Such as e.g. plots or images. It is called by the main UI on startup to create the frame. + * :func:`set_window`: Assigngs the main window that is used by the main UI. This allows the user + to have full controller over all UI elements. But be warned, with great power comes great responsibility. + """ + + """ + Exposed Properties + """ + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the component has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_debug_vis_impl) + return "NotImplementedError" not in source_code + + @property + def has_vis_frame_implementation(self) -> bool: + """Whether the component has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_vis_frame_impl) + return "NotImplementedError" not in source_code + + @property + def has_window_implementation(self) -> bool: + """Whether the component has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_window_impl) + return "NotImplementedError" not in source_code + + @property + def has_env_selection_implementation(self) -> bool: + """Whether the component has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_env_selection_impl) + return "NotImplementedError" not in source_code + + """ + Exposed Setters + """ + + def set_env_selection(self, env_selection: int) -> bool: + """Sets the selected environment id. + + This function is called by the main UI when the user selects a different environment. + + Args: + env_selection: The currently selected environment id. + + Returns: + Whether the environment selection was successfully set. False if the component + does not support environment selection. + """ + # check if environment selection is supported + if not self.has_env_selection_implementation: + return False + # set environment selection + self._set_env_selection_impl(env_selection) + return True + + def set_window(self, window: omni.ui.Window) -> bool: + """Sets the current main ui window. + + This function is called by the main UI when the window is created. It allows the component + to add custom UI elements to the window or to control the window and its elements. + + Args: + window: The ui window. + + Returns: + Whether the window was successfully set. False if the component + does not support this functionality. + """ + # check if window is supported + if not self.has_window_implementation: + return False + # set window + self._set_window_impl(window) + return True + + def set_vis_frame(self, vis_frame: omni.ui.Frame) -> bool: + """Sets the debug visualization frame. + + This function is called by the main UI when the window is created. It allows the component + to modify a small frame within the orbit tab that can be used to visualize debug information. + + Args: + vis_frame: The debug visualization frame. + + Returns: + Whether the debug visualization frame was successfully set. False if the component + does not support debug visualization. + """ + # check if debug visualization is supported + if not self.has_vis_frame_implementation: + return False + # set debug visualization frame + self._set_vis_frame_impl(vis_frame) + return True + + """ + Internal Implementation + """ + + def _set_env_selection_impl(self, env_idx: int): + """Set the environment selection.""" + raise NotImplementedError(f"Environment selection is not implemented for {self.__class__.__name__}.") + + def _set_window_impl(self, window: omni.ui.Window): + """Set the window.""" + raise NotImplementedError(f"Window is not implemented for {self.__class__.__name__}.") + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization state.""" + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") + + def _set_vis_frame_impl(self, vis_frame: omni.ui.Frame): + """Set debug visualization into visualization objects. + + This function is responsible for creating the visualization objects if they don't exist + and input ``debug_vis`` is True. If the visualization objects exist, the function should + set their visibility into the stage. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") diff --git a/source/uwlab/uwlab/ui/widgets/ui_widget_wrapper.py b/source/uwlab/uwlab/ui/widgets/ui_widget_wrapper.py new file mode 100644 index 0000000..501db28 --- /dev/null +++ b/source/uwlab/uwlab/ui/widgets/ui_widget_wrapper.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# This 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 + +from typing import TYPE_CHECKING + +import omni + +if TYPE_CHECKING: + import omni.ui + + +class UIWidgetWrapper: + """ + Base class for creating wrappers around any subclass of omni.ui.Widget in order to provide an easy interface + for creating and managing specific types of widgets such as state buttons or file pickers. + """ + + def __init__(self, container_frame: omni.ui.Frame): + self._container_frame = container_frame + + @property + def container_frame(self) -> omni.ui.Frame: + return self._container_frame + + @property + def enabled(self) -> bool: + return self.container_frame.enabled + + @enabled.setter + def enabled(self, value: bool): + self.container_frame.enabled = value + + @property + def visible(self) -> bool: + return self.container_frame.visible + + @visible.setter + def visible(self, value: bool): + self.container_frame.visible = value + + def cleanup(self): + """ + Perform any necessary cleanup + """ + pass diff --git a/source/uwlab/uwlab/utils/__init__.py b/source/uwlab/uwlab/utils/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab/uwlab/utils/__init__.py +++ b/source/uwlab/uwlab/utils/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/utils/datasets/torch_dataset_file_handler.py b/source/uwlab/uwlab/utils/datasets/torch_dataset_file_handler.py new file mode 100644 index 0000000..e1ec6de --- /dev/null +++ b/source/uwlab/uwlab/utils/datasets/torch_dataset_file_handler.py @@ -0,0 +1,111 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Torch Dataset File Handler +This module provides a dataset file handler that saves data directly in the torch format. +""" + +from __future__ import annotations + +import os +import torch +from collections.abc import Iterable +from typing import Any + +from isaaclab.utils.datasets.dataset_file_handler_base import DatasetFileHandlerBase, EpisodeData + + +class TorchDatasetFileHandler(DatasetFileHandlerBase): + """ + Dataset file handler that saves data directly in torch format as a torch file. + """ + + def __init__(self): + self._file_path: str | None = None + self._episode_data: dict[str, Any] = {} + self._episode_count: int = 0 + + def create(self, file_path: str, env_name: str | None = None): + """Create a new dataset file.""" + self._file_path = file_path + self._episode_data = {} + self._episode_count = 0 + + # Ensure directory exists + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + def open(self, file_path: str, mode: str = "r"): + """Open an existing dataset file.""" + if mode == "r": + if os.path.exists(file_path): + self._episode_data = torch.load(file_path) + self._file_path = file_path + else: + raise FileNotFoundError(f"Dataset file not found: {file_path}") + else: + self._file_path = file_path + self._episode_data = {} + + def get_env_name(self) -> str | None: + """Get the environment name.""" + return + + def get_episode_names(self) -> Iterable[str]: + """Get the names of the episodes in the file.""" + return [] + + def get_num_episodes(self) -> int: + """Get number of episodes in the file.""" + return self._episode_count + + def write_episode(self, episode: EpisodeData, demo_id: int | None = None): + """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. + """ + if episode.is_empty() or not episode.success: + return + + # Extract only the last entry from this episode using extend_dicts_last_entry logic + self._extend_dicts_last_entry(self._episode_data, episode.data) + + # Only increment episode count if using default indexing + if demo_id is None: + self._episode_count += 1 + + def _extend_dicts_last_entry(self, dest: dict[str, Any], src: dict[str, Any], store_data: bool = True) -> None: + """Extend destination dictionary with last entry from source dictionary.""" + for key in src.keys(): + if isinstance(src[key], dict): + if key not in dest: + dest[key] = {} + self._extend_dicts_last_entry(dest[key], src[key], store_data) + else: + if key not in dest: + dest[key] = [] + if store_data: + dest[key].extend(src[key][-1:]) # Only take the last entry from src[key] + + def load_episode(self, episode_name: str, device: str = "cpu") -> EpisodeData | None: + """Load episode data from the file.""" + # Not applicable for this handler since we store aggregated data + raise NotImplementedError("Load episode not supported for preprocessed format") + + def flush(self): + """Flush any pending data to disk.""" + if self._file_path and self._episode_data: + torch.save(self._episode_data, self._file_path) + + def close(self): + """Close the dataset file handler.""" + self.flush() + self._episode_data = {} + self._file_path = None + + def add_env_args(self, env_args: dict): + pass diff --git a/source/uwlab/uwlab/utils/io/__init__.py b/source/uwlab/uwlab/utils/io/__init__.py index b3dfe19..3e23f81 100644 --- a/source/uwlab/uwlab/utils/io/__init__.py +++ b/source/uwlab/uwlab/utils/io/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/utils/io/encode.py b/source/uwlab/uwlab/utils/io/encode.py index 473ff25..f06a150 100644 --- a/source/uwlab/uwlab/utils/io/encode.py +++ b/source/uwlab/uwlab/utils/io/encode.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/utils/math.py b/source/uwlab/uwlab/utils/math.py index b0ba1ba..26b05e0 100644 --- a/source/uwlab/uwlab/utils/math.py +++ b/source/uwlab/uwlab/utils/math.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/utils/noise/__init__.py b/source/uwlab/uwlab/utils/noise/__init__.py index 8de4351..55a92b0 100644 --- a/source/uwlab/uwlab/utils/noise/__init__.py +++ b/source/uwlab/uwlab/utils/noise/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/utils/noise/noise_cfg.py b/source/uwlab/uwlab/utils/noise/noise_cfg.py index d62ea5d..6845c21 100644 --- a/source/uwlab/uwlab/utils/noise/noise_cfg.py +++ b/source/uwlab/uwlab/utils/noise/noise_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab/uwlab/utils/noise/noise_model.py b/source/uwlab/uwlab/utils/noise/noise_model.py index eeb9d8a..48c4985 100644 --- a/source/uwlab/uwlab/utils/noise/noise_model.py +++ b/source/uwlab/uwlab/utils/noise/noise_model.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/config/extension.toml b/source/uwlab_assets/config/extension.toml index b3f788e..dd81902 100644 --- a/source/uwlab_assets/config/extension.toml +++ b/source/uwlab_assets/config/extension.toml @@ -6,7 +6,7 @@ version = "0.5.2" title = "UW Lab Assets" description="Extension containing configuration instances of different assets and sensors" readme = "docs/README.md" -repository = "https://github.com/UW-Lab/UWLab" +repository = "https://github.com/uw-lab/UWLab" category = "robotics" keywords = ["isaaclab", "robotics", "rl", "il", "learning"] diff --git a/source/uwlab_assets/docs/CHANGELOG.rst b/source/uwlab_assets/docs/CHANGELOG.rst index 8810c64..cb39cdb 100644 --- a/source/uwlab_assets/docs/CHANGELOG.rst +++ b/source/uwlab_assets/docs/CHANGELOG.rst @@ -25,7 +25,7 @@ Added Added ^^^^^ -* added custom franka setup at :folder:`uwlab_asset.franka` +* added custom franka setup at :mod:`uwlab_asset.franka` 0.4.1 (2024-09-01) ~~~~~~~~~~~~~~~~~~ @@ -33,7 +33,7 @@ Added Added ^^^^^ -* Fixed the reversed y axis in Tycho Teleop Cfg at :const:`uwlab_asset.tycho.action.TELEOP_CFG` +* Fixed the reversed y axis in Tycho Teleop Cfg at :data:`uwlab_asset.tycho.action.TELEOP_CFG` 0.4.0 (2024-09-01) ~~~~~~~~~~~~~~~~~~ @@ -96,8 +96,8 @@ Added Changed ^^^^^^^ -* Bug fix at :const:`uwlab_assets.robots.leap.actions.LEAP_JOINT_POSITION` - and :const:`uwlab_assets.robots.leap.actions.LEAP_JOINT_EFFORT` because +* Bug fix at :data:`uwlab_assets.robots.leap.actions.LEAP_JOINT_POSITION` + and :data:`uwlab_assets.robots.leap.actions.LEAP_JOINT_EFFORT` because previous version did not include all joint name. it used to be ``joint_names=["j.*"]`` now becomes ``joint_names=["w.*", "j.*"]`` diff --git a/source/uwlab_assets/setup.py b/source/uwlab_assets/setup.py index 2041990..6e7d075 100644 --- a/source/uwlab_assets/setup.py +++ b/source/uwlab_assets/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/test/test_valid_configs.py b/source/uwlab_assets/test/test_valid_configs.py index c2fb237..e120255 100644 --- a/source/uwlab_assets/test/test_valid_configs.py +++ b/source/uwlab_assets/test/test_valid_configs.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,7 @@ """Launch Isaac Sim Simulator first.""" -from isaaclab.app import AppLauncher, run_tests +from isaaclab.app import AppLauncher # launch the simulator app_launcher = AppLauncher(headless=True) @@ -17,54 +17,47 @@ """Rest everything follows.""" -import unittest - -import isaaclab_assets as lab_assets # noqa: F401 - +# Define a fixture to replace setUpClass +import pytest from isaaclab.assets import AssetBase, AssetBaseCfg from isaaclab.sim import build_simulation_context - -class TestValidEntitiesConfigs(unittest.TestCase): - """Test cases for all registered entities configurations.""" - - @classmethod - def setUpClass(cls): - # load all registered entities configurations from the module - cls.registered_entities: dict[str, AssetBaseCfg] = {} - # inspect all classes from the module - for obj_name in dir(lab_assets): - obj = getattr(lab_assets, obj_name) - # store all registered entities configurations - if isinstance(obj, AssetBaseCfg): - cls.registered_entities[obj_name] = obj - # print all existing entities names - print(">>> All registered entities:", list(cls.registered_entities.keys())) - - """ - Test fixtures. - """ - - def test_asset_configs(self): - """Check all registered asset configurations.""" - # iterate over all registered assets - for asset_name, entity_cfg in self.registered_entities.items(): - for device in ("cuda:0", "cpu"): - with self.subTest(asset_name=asset_name, device=device): - with build_simulation_context(device=device, auto_add_lighting=True) as sim: - # print the asset name - print(f">>> Testing entity {asset_name} on device {device}") - # name the prim path - entity_cfg.prim_path = "/World/asset" - # create the asset / sensors - entity: AssetBase = entity_cfg.class_type(entity_cfg) # type: ignore - - # play the sim - sim.reset() - - # check asset is initialized successfully - self.assertTrue(entity.is_initialized) - - -if __name__ == "__main__": - run_tests() +import uwlab_assets as lab_assets # noqa: F401 + + +@pytest.fixture(scope="module") +def registered_entities(): + # load all registered entities configurations from the module + registered_entities: dict[str, AssetBaseCfg] = {} + # inspect all classes from the module + for obj_name in dir(lab_assets): + obj = getattr(lab_assets, obj_name) + # store all registered entities configurations + if isinstance(obj, AssetBaseCfg): + registered_entities[obj_name] = obj + # print all existing entities names + print(">>> All registered entities:", list(registered_entities.keys())) + return registered_entities + + +# Add parameterization for the device +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_asset_configs(registered_entities, device): + """Check all registered asset configurations.""" + # iterate over all registered assets + for asset_name, entity_cfg in registered_entities.items(): + # Use pytest's subtests + with build_simulation_context(device=device, auto_add_lighting=True) as sim: + sim._app_control_on_stop_handle = None + # print the asset name + print(f">>> Testing entity {asset_name} on device {device}") + # name the prim path + entity_cfg.prim_path = "/World/asset" + # create the asset / sensors + entity: AssetBase = entity_cfg.class_type(entity_cfg) # type: ignore + + # play the sim + sim.reset() + + # check asset is initialized successfully + assert entity.is_initialized diff --git a/source/uwlab_assets/uwlab_assets/__init__.py b/source/uwlab_assets/uwlab_assets/__init__.py index 896f6a6..b1f73f9 100644 --- a/source/uwlab_assets/uwlab_assets/__init__.py +++ b/source/uwlab_assets/uwlab_assets/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/props/workbench/workbench_conversion_cfg.py b/source/uwlab_assets/uwlab_assets/props/workbench/workbench_conversion_cfg.py index 19a563c..8eec0fd 100644 --- a/source/uwlab_assets/uwlab_assets/props/workbench/workbench_conversion_cfg.py +++ b/source/uwlab_assets/uwlab_assets/props/workbench/workbench_conversion_cfg.py @@ -1,10 +1,8 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from typing import List - from uwlab.sim.converters import MeshConverterCfg from uwlab.sim.converters.common_material_property_cfg import PVCCfg, SteelCfg from uwlab.sim.spawners.materials import common_materials_cfg as common_materials @@ -35,4 +33,4 @@ visual_material_props=common_materials.SteelVisualMaterialCfg(diffuse_color=(0.1, 0.1, 0.4)), ) -WORKBENCH_CONVERSION_CFG: List[MeshConverterCfg] = [BLOCK, BOX, SHELF] +WORKBENCH_CONVERSION_CFG: list[MeshConverterCfg] = [BLOCK, BOX, SHELF] diff --git a/source/uwlab_assets/uwlab_assets/robots/cartpole.py b/source/uwlab_assets/uwlab_assets/robots/cartpole.py new file mode 100644 index 0000000..7cf36ee --- /dev/null +++ b/source/uwlab_assets/uwlab_assets/robots/cartpole.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configuration for a simple Cartpole robot.""" + + +import isaaclab.sim as sim_utils +from isaaclab.actuators import ImplicitActuatorCfg +from isaaclab.assets import ArticulationCfg +from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR + +## +# Configuration +## + +CARTPOLE_CFG = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/Classic/Cartpole/cartpole.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + rigid_body_enabled=True, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=100.0, + enable_gyroscopic_forces=True, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + sleep_threshold=0.005, + stabilization_threshold=0.001, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 2.0), joint_pos={"slider_to_cart": 0.0, "cart_to_pole": 0.0} + ), + actuators={ + "cart_actuator": ImplicitActuatorCfg( + joint_names_expr=["slider_to_cart"], + effort_limit_sim=400.0, + stiffness=0.0, + damping=10.0, + ), + "pole_actuator": ImplicitActuatorCfg( + joint_names_expr=["cart_to_pole"], effort_limit_sim=400.0, stiffness=0.0, damping=0.0 + ), + }, +) +"""Configuration for a simple Cartpole robot.""" diff --git a/source/uwlab_assets/uwlab_assets/robots/franka/__init__.py b/source/uwlab_assets/uwlab_assets/robots/franka/__init__.py index 63b4a54..3fc6051 100644 --- a/source/uwlab_assets/uwlab_assets/robots/franka/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/franka/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/franka/action.py b/source/uwlab_assets/uwlab_assets/robots/franka/action.py index 7ba8385..86f1015 100644 --- a/source/uwlab_assets/uwlab_assets/robots/franka/action.py +++ b/source/uwlab_assets/uwlab_assets/robots/franka/action.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/franka/teleop.py b/source/uwlab_assets/uwlab_assets/robots/franka/teleop.py index fbb4535..1ef3a46 100644 --- a/source/uwlab_assets/uwlab_assets/robots/franka/teleop.py +++ b/source/uwlab_assets/uwlab_assets/robots/franka/teleop.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + from uwlab.devices import KeyboardCfg, TeleopCfg diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/__init__.py b/source/uwlab_assets/uwlab_assets/robots/leap/__init__.py index 979df22..4520a4f 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/__init__.py @@ -1,7 +1,7 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from .actions import * -from .leap import FRAME_EE, IMPLICIT_LEAP, IMPLICIT_LEAP6D +from .leap import FRAME_EE, IMPLICIT_LEAP diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/actions.py b/source/uwlab_assets/uwlab_assets/robots/leap/actions.py index 63b56e0..58a47d8 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/actions.py @@ -1,7 +1,13 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + from __future__ import annotations from isaaclab.envs.mdp.actions.actions_cfg import JointEffortActionCfg, JointPositionActionCfg from isaaclab.utils import configclass + from uwlab.controllers.differential_ik_cfg import MultiConstraintDifferentialIKControllerCfg from uwlab.envs.mdp.actions.actions_cfg import MultiConstraintsDifferentialInverseKinematicsActionCfg diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/__init__.py b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/__init__.py index 80d7de2..bd8dd59 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_client.py b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_client.py index c7414bf..1307431 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_client.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_client.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -8,7 +8,7 @@ import logging import numpy as np import time -from typing import Optional, Sequence, Tuple, Union +from collections.abc import Sequence PROTOCOL_VERSION = 2.0 @@ -74,9 +74,9 @@ def __init__( port: str = "/dev/ttyUSB0", baudrate: int = 1000000, lazy_connect: bool = False, - pos_scale: Optional[float] = None, - vel_scale: Optional[float] = None, - cur_scale: Optional[float] = None, + pos_scale: float | None = None, + vel_scale: float | None = None, + cur_scale: float | None = None, ): """Initializes a new client. Args: @@ -211,7 +211,7 @@ def set_torque_enabled( time.sleep(retry_interval) retries -= 1 - def read_pos_vel_cur(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + def read_pos_vel_cur(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Returns the current positions and velocities.""" return self._pos_vel_cur_reader.read() @@ -262,7 +262,7 @@ def write_byte( errored_ids.append(motor_id) return errored_ids - def sync_write(self, motor_ids: Sequence[int], values: Sequence[Union[int, float]], address: int, size: int): + def sync_write(self, motor_ids: Sequence[int], values: Sequence[int | float], address: int, size: int): """Writes values to a group of motors. Args: motor_ids: The motor IDs to write to. @@ -302,9 +302,9 @@ def check_connected(self): def handle_packet_result( self, comm_result: int, - dxl_error: Optional[int] = None, - dxl_id: Optional[int] = None, - context: Optional[str] = None, + dxl_error: int | None = None, + dxl_id: int | None = None, + context: str | None = None, ): """Handles the result from a communication request.""" error_message = None diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver.py b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver.py index 051089e..62f79d1 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver_cfg.py b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver_cfg.py index 7c2fa96..64e0ae0 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver_cfg.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/articulation_drive/dynamixel_driver_cfg.py @@ -1,14 +1,15 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable from isaaclab.utils import configclass + from uwlab.assets.articulation.articulation_drive import ArticulationDriveCfg from .dynamixel_driver import DynamixelDriver diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/leap.py b/source/uwlab_assets/uwlab_assets/robots/leap/leap.py index b527275..d0556c2 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/leap.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/leap.py @@ -1,10 +1,8 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg @@ -12,17 +10,14 @@ from isaaclab.sensors import FrameTransformerCfg from isaaclab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + ## # Configuration ## -# fmt: off LEAP_DEFAULT_JOINT_POS = {".*": 0.0} -LEAP6D_DEFAULT_JOINT_POS = {".*": 0.0} -# fmt: on - - LEAP_ARTICULATION = ArticulationCfg( spawn=sim_utils.UsdFileCfg( usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Robots/LeapHand/leap_hand.usd", @@ -41,22 +36,6 @@ soft_joint_pos_limit_factor=1, ) -LEAP6D_ARTICULATION = ArticulationCfg( - spawn=sim_utils.UsdFileCfg( - usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Robots/LeapHand/leap_hand_6d.usd", - activate_contact_sensors=False, - rigid_props=sim_utils.RigidBodyPropertiesCfg( - disable_gravity=True, - max_depenetration_velocity=5.0, - ), - articulation_props=sim_utils.ArticulationRootPropertiesCfg( - enabled_self_collisions=True, solver_position_iteration_count=1, solver_velocity_iteration_count=0 - ), - ), - init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(1, 0, 0, 0), joint_pos=LEAP6D_DEFAULT_JOINT_POS), - soft_joint_pos_limit_factor=1, -) - IMPLICIT_LEAP = LEAP_ARTICULATION.copy() IMPLICIT_LEAP.actuators = { "j": ImplicitActuatorCfg( @@ -70,28 +49,6 @@ ), } -IMPLICIT_LEAP6D = LEAP6D_ARTICULATION.copy() # type: ignore -IMPLICIT_LEAP6D.actuators = { - "w": ImplicitActuatorCfg( - joint_names_expr=["w.*"], - stiffness=200.0, - damping=50.0, - armature=0.001, - friction=0.2, - # velocity_limit=1, - effort_limit=50, - ), - "j": ImplicitActuatorCfg( - joint_names_expr=["j.*"], - stiffness=200.0, - damping=30.0, - armature=0.001, - friction=0.2, - # velocity_limit=8.48, - effort_limit=0.95, - ), -} - """ FRAMES diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/__init__.py b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/__init__.py index fe7e5ea..6abced5 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/__init__.py b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/__init__.py index ddb1b62..f251b10 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions.py b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions.py index 9b32c98..13eb145 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions_cfg.py b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions_cfg.py index 0efb3c9..34a68ba 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions_cfg.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/mdp/actions/actions_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/leap/teleop.py b/source/uwlab_assets/uwlab_assets/robots/leap/teleop.py index 8402d89..8663774 100644 --- a/source/uwlab_assets/uwlab_assets/robots/leap/teleop.py +++ b/source/uwlab_assets/uwlab_assets/robots/leap/teleop.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + from uwlab.devices import KeyboardCfg, RealsenseT265Cfg, RokokoGlovesCfg, TeleopCfg diff --git a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/__init__.py b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/__init__.py index 2c1b17f..7670172 100644 --- a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/__init__.py b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver.py b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver.py index 0cd926f..14be9d9 100644 --- a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver.py +++ b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver_cfg.py b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver_cfg.py index 246967a..b0b1aa2 100644 --- a/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver_cfg.py +++ b/source/uwlab_assets/uwlab_assets/robots/robotiq_gripper/articulation_drive/robotiq_driver_cfg.py @@ -1,13 +1,14 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -from typing import Callable +from collections.abc import Callable from isaaclab.utils import configclass + from uwlab.assets.articulation.articulation_drive import ArticulationDriveCfg from .robotiq_driver import RobotiqDriver diff --git a/source/uwlab_assets/uwlab_assets/robots/spot/__init__.py b/source/uwlab_assets/uwlab_assets/robots/spot/__init__.py index 1a70504..b82b823 100644 --- a/source/uwlab_assets/uwlab_assets/robots/spot/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/spot/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/spot/actions.py b/source/uwlab_assets/uwlab_assets/robots/spot/actions.py index 8f3bf37..b88c6fb 100644 --- a/source/uwlab_assets/uwlab_assets/robots/spot/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/spot/actions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/spot/arm_spot.py b/source/uwlab_assets/uwlab_assets/robots/spot/arm_spot.py index b6d12e7..17c02ae 100644 --- a/source/uwlab_assets/uwlab_assets/robots/spot/arm_spot.py +++ b/source/uwlab_assets/uwlab_assets/robots/spot/arm_spot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,12 +10,12 @@ * :obj:`SPOT_CFG`: The Spot robot with delay PD and remote PD actuators. """ -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.actuators import DelayedPDActuatorCfg, ImplicitActuatorCfg, RemotizedPDActuatorCfg from isaaclab.assets.articulation import ArticulationCfg +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + # Note: This data was collected by the Boston Dynamics AI Institute. joint_parameter_lookup = [ [-2.792900, -24.776718, 37.165077], diff --git a/source/uwlab_assets/uwlab_assets/robots/spot/spot.py b/source/uwlab_assets/uwlab_assets/robots/spot/spot.py index 27be726..a8c2ad0 100644 --- a/source/uwlab_assets/uwlab_assets/robots/spot/spot.py +++ b/source/uwlab_assets/uwlab_assets/robots/spot/spot.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,12 +10,12 @@ * :obj:`SPOT_CFG`: The Spot robot with delay PD and remote PD actuators. """ -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.actuators import DelayedPDActuatorCfg, RemotizedPDActuatorCfg from isaaclab.assets.articulation import ArticulationCfg +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + # Note: This data was collected by the Boston Dynamics AI Institute. joint_parameter_lookup = [ [-2.792900, -24.776718, 37.165077], diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/__init__.py b/source/uwlab_assets/uwlab_assets/robots/tycho/__init__.py index e6cf9cb..05698a3 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py b/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py index 04c8274..d7d5770 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/actions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/__init__.py b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/__init__.py index 400a45b..478f399 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/observations.py b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/observations.py index 7b59db5..1c9f348 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/observations.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/observations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/rewards.py b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/rewards.py index 47c0eeb..cea5680 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/rewards.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/terminations.py b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/terminations.py index 2070c5e..7d94317 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/terminations.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/mdp/terminations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/teleop.py b/source/uwlab_assets/uwlab_assets/robots/tycho/teleop.py index 4f7221d..c09902f 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/teleop.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/teleop.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + from uwlab.devices import KeyboardCfg, TeleopCfg diff --git a/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py b/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py index cf09ef1..de1d99d 100644 --- a/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py +++ b/source/uwlab_assets/uwlab_assets/robots/tycho/tycho.py @@ -1,18 +1,18 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause """Configuration for the Tycho robot.""" -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg from isaaclab.sensors import FrameTransformerCfg from isaaclab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + from isaaclab.markers.config import FRAME_MARKER_CFG # isort: skip diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5/__init__.py b/source/uwlab_assets/uwlab_assets/robots/ur5/__init__.py index 5664a59..76a0e26 100644 --- a/source/uwlab_assets/uwlab_assets/robots/ur5/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/ur5/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5/actions.py b/source/uwlab_assets/uwlab_assets/robots/ur5/actions.py index 1d42a0e..72c0250 100644 --- a/source/uwlab_assets/uwlab_assets/robots/ur5/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/ur5/actions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,6 +11,7 @@ RelativeJointPositionActionCfg, ) from isaaclab.utils import configclass + from uwlab.controllers.differential_ik_cfg import MultiConstraintDifferentialIKControllerCfg from uwlab.envs.mdp.actions.actions_cfg import ( DefaultJointPositionStaticActionCfg, diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver.py b/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver.py index 1994347..40da2b3 100644 --- a/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver.py +++ b/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver_cfg.py b/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver_cfg.py index d3b326f..1c37625 100644 --- a/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver_cfg.py +++ b/source/uwlab_assets/uwlab_assets/robots/ur5/articulation_drive/ur_driver_cfg.py @@ -1,14 +1,15 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable from isaaclab.utils import configclass + from uwlab.assets.articulation.articulation_drive import ArticulationDriveCfg from .ur_driver import URDriver diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5/teleop.py b/source/uwlab_assets/uwlab_assets/robots/ur5/teleop.py index 0e15034..70e2c48 100644 --- a/source/uwlab_assets/uwlab_assets/robots/ur5/teleop.py +++ b/source/uwlab_assets/uwlab_assets/robots/ur5/teleop.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + from uwlab.devices import KeyboardCfg, TeleopCfg diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5/ur5.py b/source/uwlab_assets/uwlab_assets/robots/ur5/ur5.py index fc1a24c..985940a 100644 --- a/source/uwlab_assets/uwlab_assets/robots/ur5/ur5.py +++ b/source/uwlab_assets/uwlab_assets/robots/ur5/ur5.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,12 +10,12 @@ * :obj:`UR5_CFG`: Ur5 robot """ -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + UR5_DEFAULT_JOINT_POS = { "shoulder_pan_joint": 0.0, "shoulder_lift_joint": -1.5708, diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/__init__.py b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/__init__.py new file mode 100644 index 0000000..807eefb --- /dev/null +++ b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +from .actions import * +from .ur5e_robotiq_2f85_gripper import EXPLICIT_UR5E_ROBOTIQ_2F85, IMPLICIT_UR5E_ROBOTIQ_2F85, ROBOTIQ_2F85 diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/actions.py b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/actions.py new file mode 100644 index 0000000..50b99e8 --- /dev/null +++ b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/actions.py @@ -0,0 +1,116 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from isaaclab.envs.mdp.actions.actions_cfg import ( + BinaryJointPositionActionCfg, + JointPositionActionCfg, + RelativeJointPositionActionCfg, +) +from isaaclab.utils import configclass + +from uwlab.controllers.differential_ik_cfg import MultiConstraintDifferentialIKControllerCfg +from uwlab.envs.mdp.actions.actions_cfg import ( + DefaultJointPositionStaticActionCfg, + MultiConstraintsDifferentialInverseKinematicsActionCfg, +) + +""" +UR5E ROBOTIQ 2F85 ACTIONS +""" +UR5E_JOINT_POSITION: JointPositionActionCfg = JointPositionActionCfg( + asset_name="robot", + joint_names=["shoulder.*", "elbow.*", "wrist.*"], + scale=1.0, + use_default_offset=False, +) + +UR5E_RELATIVE_JOINT_POSITION: RelativeJointPositionActionCfg = RelativeJointPositionActionCfg( + asset_name="robot", + joint_names=["shoulder.*", "elbow.*", "wrist.*"], + scale={ + "(?!wrist_3_joint).*": 0.02, + "wrist_3_joint": 0.2, + }, + use_zero_offset=True, + clip={ + "(?!wrist_3_joint).*": (-0.5, 0.5), + "wrist_3_joint": (-5.0, 5.0), + }, +) + +UR5E_MC_IKABSOLUTE_ARM = MultiConstraintsDifferentialInverseKinematicsActionCfg( + asset_name="robot", + joint_names=["shoulder.*", "elbow.*", "wrist.*"], + body_name=["robotiq_base_link"], + controller=MultiConstraintDifferentialIKControllerCfg( + command_type="pose", use_relative_mode=False, ik_method="dls" + ), + scale=1, +) + +UR5E_MC_IKDELTA_ARM = MultiConstraintsDifferentialInverseKinematicsActionCfg( + asset_name="robot", + joint_names=["joint.*"], + body_name=["robotiq_base_link"], + controller=MultiConstraintDifferentialIKControllerCfg(command_type="pose", use_relative_mode=True, ik_method="dls"), + scale=0.5, +) + +ROBOTIQ_GRIPPER_BINARY_ACTIONS = BinaryJointPositionActionCfg( + asset_name="robot", + joint_names=["finger_joint"], + open_command_expr={"finger_joint": 0.0}, + close_command_expr={"finger_joint": 0.785398}, +) + +ROBOTIQ_COMPLIANT_JOINTS = DefaultJointPositionStaticActionCfg( + asset_name="robot", joint_names=["left_inner_finger_joint", "right_inner_finger_joint"] +) + +ROBOTIQ_MC_IK_ABSOLUTE = MultiConstraintsDifferentialInverseKinematicsActionCfg( + asset_name="robot", + joint_names=["joint.*"], + body_name=["left_inner_finger", "right_inner_finger"], + controller=MultiConstraintDifferentialIKControllerCfg( + command_type="pose", use_relative_mode=False, ik_method="dls" + ), + scale=1, +) + + +@configclass +class Ur5eRobotiq2f85IkAbsoluteAction: + arm = UR5E_MC_IKABSOLUTE_ARM + gripper = ROBOTIQ_GRIPPER_BINARY_ACTIONS + compliant_joints = ROBOTIQ_COMPLIANT_JOINTS + + +@configclass +class Ur5eRobotiq2f85McIkDeltaAction: + arm = UR5E_MC_IKDELTA_ARM + gripper = ROBOTIQ_GRIPPER_BINARY_ACTIONS + compliant_joints = ROBOTIQ_COMPLIANT_JOINTS + + +@configclass +class Ur5eRobotiq2f85JointPositionAction: + arm = UR5E_JOINT_POSITION + gripper = ROBOTIQ_GRIPPER_BINARY_ACTIONS + compliant_joints = ROBOTIQ_COMPLIANT_JOINTS + + +@configclass +class Ur5eRobotiq2f85RelativeJointPositionAction: + arm = UR5E_RELATIVE_JOINT_POSITION + gripper = ROBOTIQ_GRIPPER_BINARY_ACTIONS + compliant_joints = ROBOTIQ_COMPLIANT_JOINTS + + +@configclass +class Robotiq2f85BinaryGripperAction: + gripper = ROBOTIQ_GRIPPER_BINARY_ACTIONS + compliant_joints = ROBOTIQ_COMPLIANT_JOINTS diff --git a/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/ur5e_robotiq_2f85_gripper.py b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/ur5e_robotiq_2f85_gripper.py new file mode 100644 index 0000000..f961743 --- /dev/null +++ b/source/uwlab_assets/uwlab_assets/robots/ur5e_robotiq_gripper/ur5e_robotiq_2f85_gripper.py @@ -0,0 +1,143 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configuration for the UR5 robots. + +The following configurations are available: + +* :obj:`UR5E_CFG`: Ur5e robot +""" + +import isaaclab.sim as sim_utils +from isaaclab.actuators import ImplicitActuatorCfg +from isaaclab.assets.articulation import ArticulationCfg + +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + +ROBOTIQ_2F85_DEFAULT_JOINT_POS = { + "finger_joint": 0.0, + "right_outer.*": 0.0, + "left_outer.*": 0.0, + "left_inner_finger_knuckle_joint": 0.0, + "right_inner_finger_knuckle_joint": 0.0, + "left_inner_finger_joint": -0.785398, + "right_inner_finger_joint": 0.785398, +} + +UR5E_DEFAULT_JOINT_POS = { + "shoulder_pan_joint": 0.0, + "shoulder_lift_joint": -1.5708, + "elbow_joint": 1.5708, + "wrist_1_joint": -1.5708, + "wrist_2_joint": -1.5708, + "wrist_3_joint": -1.5708, + **ROBOTIQ_2F85_DEFAULT_JOINT_POS, +} + +UR5E_ARTICULATION = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Robots/UniversalRobots/Ur5e2f85RobotiqGripper/ur5e_robotiq_gripper_d415_mount_safety.usd", + activate_contact_sensors=False, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=36, solver_velocity_iteration_count=0 + ), + ), + init_state=ArticulationCfg.InitialStateCfg(pos=(0, 0, 0), rot=(1, 0, 0, 0), joint_pos=UR5E_DEFAULT_JOINT_POS), + soft_joint_pos_limit_factor=1, +) + +ROBOTIQ_2F85 = ArticulationCfg( + prim_path="{ENV_REGEX_NS}/RobotiqGripper", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Robots/UniversalRobots/2f85RobotiqGripper/robotiq_2f85_gripper.usd", + activate_contact_sensors=False, + 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=36, solver_velocity_iteration_count=0 + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0, 0, 0.1), rot=(1, 0, 0, 0), joint_pos=ROBOTIQ_2F85_DEFAULT_JOINT_POS + ), + actuators={ + "gripper": ImplicitActuatorCfg( + joint_names_expr=["finger_joint"], + stiffness=17, + damping=5, + effort_limit_sim=165, + ), + "inner_finger": ImplicitActuatorCfg( + joint_names_expr=[".*_inner_finger_joint"], + stiffness=0.2, + damping=0.02, + effort_limit_sim=0.5, + ), + }, + soft_joint_pos_limit_factor=1, +) + +IMPLICIT_UR5E_ROBOTIQ_2F85 = UR5E_ARTICULATION.copy() # type: ignore +IMPLICIT_UR5E_ROBOTIQ_2F85.actuators = { + "arm": ImplicitActuatorCfg( + joint_names_expr=["shoulder.*", "elbow.*", "wrist.*"], + stiffness={ + "shoulder_pan_joint": 4.63, + "shoulder_lift_joint": 5.41, + "elbow_joint": 8.06, + "wrist_1_joint": 7.28, + "wrist_2_joint": 8.04, + "wrist_3_joint": 7.18, + }, + damping={ + "shoulder_pan_joint": 8.84, + "shoulder_lift_joint": 6.47, + "elbow_joint": 9.46, + "wrist_1_joint": 2.80, + "wrist_2_joint": 2.41, + "wrist_3_joint": 1.90, + }, + velocity_limit_sim=3.14, + effort_limit_sim={ + "shoulder_pan_joint": 150.0, + "shoulder_lift_joint": 150.0, + "elbow_joint": 150.0, + "wrist_1_joint": 28.0, + "wrist_2_joint": 28.0, + "wrist_3_joint": 28.0, + }, + armature=0.01, + ), + "gripper": ROBOTIQ_2F85.actuators["gripper"], + "inner_finger": ROBOTIQ_2F85.actuators["inner_finger"], +} + +EXPLICIT_UR5E_ROBOTIQ_2F85 = UR5E_ARTICULATION.copy() # type: ignore +EXPLICIT_UR5E_ROBOTIQ_2F85.actuators = { + "arm": ImplicitActuatorCfg( + joint_names_expr=["shoulder.*", "elbow.*", "wrist.*"], + stiffness=0.0, + damping=0.0, + velocity_limit_sim=3.14, + effort_limit_sim={ + "shoulder_pan_joint": 150.0, + "shoulder_lift_joint": 150.0, + "elbow_joint": 150.0, + "wrist_1_joint": 28.0, + "wrist_2_joint": 28.0, + "wrist_3_joint": 28.0, + }, + armature=0.01, + ), + "gripper": ROBOTIQ_2F85.actuators["gripper"], + "inner_finger": ROBOTIQ_2F85.actuators["inner_finger"], +} diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm/__init__.py b/source/uwlab_assets/uwlab_assets/robots/xarm/__init__.py index 2c1b17f..7670172 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/__init__.py b/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/__init__.py index eeb9d2b..be92293 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver.py b/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver.py index 3bdd122..7d72691 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver_cfg.py b/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver_cfg.py index a33c0b8..1eacb24 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver_cfg.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm/articulation_drive/xarm_driver_cfg.py @@ -1,14 +1,15 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +from collections.abc import Callable from dataclasses import MISSING -from typing import Callable from isaaclab.utils import configclass + from uwlab.assets.articulation.articulation_drive import ArticulationDriveCfg from .xarm_driver import XarmDriver diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/__init__.py b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/__init__.py index 49f5607..f790e1e 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/actions.py b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/actions.py index 0d66ba1..2bc8ef3 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/actions.py @@ -1,14 +1,15 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - from isaaclab.envs.mdp.actions.actions_cfg import JointPositionActionCfg from isaaclab.utils import configclass + +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + from uwlab.controllers.differential_ik_cfg import MultiConstraintDifferentialIKControllerCfg from uwlab.envs.mdp.actions.actions_cfg import ( MultiConstraintsDifferentialInverseKinematicsActionCfg, diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/teleop.py b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/teleop.py index 0cb9ef4..e9070d6 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/teleop.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/teleop.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + from uwlab.devices import KeyboardCfg, RealsenseT265Cfg, RokokoGlovesCfg, TeleopCfg diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py index e547d50..bf4e0e0 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_leap/xarm_leap.py @@ -1,10 +1,8 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets.articulation import ArticulationCfg @@ -12,6 +10,8 @@ from isaaclab.sensors import FrameTransformerCfg from isaaclab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + ## # Configuration ## diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/__init__.py b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/__init__.py index 8aa6f55..2c84bfe 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/__init__.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/actions.py b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/actions.py index e94bf66..b3241a1 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/actions.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/actions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/teleop.py b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/teleop.py index b30d788..383bd9b 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/teleop.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/teleop.py @@ -1,10 +1,11 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + from uwlab.devices import KeyboardCfg, TeleopCfg diff --git a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py index 1622b31..3d69748 100644 --- a/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py +++ b/source/uwlab_assets/uwlab_assets/robots/xarm_uf_gripper/xarm_uf_gripper.py @@ -1,12 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -"""Configuration for the Xarm 5 with UFactory gripper -""" - -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR +"""Configuration for the Xarm 5 with UFactory gripper""" import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg @@ -14,6 +11,8 @@ from isaaclab.markers.config import FRAME_MARKER_CFG from isaaclab.sensors import FrameTransformerCfg +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + ## # Configuration ## diff --git a/source/uwlab_rl/config/extension.toml b/source/uwlab_rl/config/extension.toml index 761d86e..b6c3f73 100644 --- a/source/uwlab_rl/config/extension.toml +++ b/source/uwlab_rl/config/extension.toml @@ -1,13 +1,13 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.1.2" +version = "0.1.3" # Description title = "UW Lab RL" description="Extension containing reinforcement learning related utilities." readme = "docs/README.md" -repository = "https://github.com/UW-Lab/UWLab" +repository = "https://github.com/uw-lab/UWLab" category = "robotics" keywords = ["robotics", "rl", "wrappers", "learning"] diff --git a/source/uwlab_rl/docs/CHANGELOG.rst b/source/uwlab_rl/docs/CHANGELOG.rst index 628d49a..1ddac4d 100644 --- a/source/uwlab_rl/docs/CHANGELOG.rst +++ b/source/uwlab_rl/docs/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog --------- +0.1.3 (2025-11-09) +~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Point rsl_rl installation to UWLab custom link that implements gsde. + + + 0.1.2 (2025-03-23) ~~~~~~~~~~~~~~~~~~ diff --git a/source/uwlab_rl/setup.py b/source/uwlab_rl/setup.py index 12c6c11..b66ca58 100644 --- a/source/uwlab_rl/setup.py +++ b/source/uwlab_rl/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -25,7 +25,11 @@ PYTORCH_INDEX_URL = ["https://download.pytorch.org/whl/cu118"] # Extra dependencies for RL agents -EXTRAS_REQUIRE = {} +EXTRAS_REQUIRE = { + "rsl-rl": [ + "rsl-rl-lib @ git+https://github.com/zoctipus/rsl_rl.git@master", + ], +} # Cumulation of all extra-requires EXTRAS_REQUIRE["all"] = list(itertools.chain.from_iterable(EXTRAS_REQUIRE.values())) diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/__init__.py b/source/uwlab_rl/uwlab_rl/rsl_rl/__init__.py index 14d2e33..065f581 100644 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/__init__.py +++ b/source/uwlab_rl/uwlab_rl/rsl_rl/__init__.py @@ -1,6 +1,6 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from .rl_cfg import BehaviorCloningCfg, OffPolicyAlgorithmCfg, RslRlFancyPpoAlgorithmCfg, SymmetryCfg +from .rl_cfg import BehaviorCloningCfg, OffPolicyAlgorithmCfg, RslRlFancyPpoAlgorithmCfg diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/__init__.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/__init__.py deleted file mode 100644 index 4e991c7..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Main module for the rsl_rl package.""" diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/__init__.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/__init__.py deleted file mode 100644 index 9aef4ca..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Implementation of different RL agents.""" diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/extensible_ppo.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/extensible_ppo.py deleted file mode 100644 index ad914ac..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/extensible_ppo.py +++ /dev/null @@ -1,640 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import torch -import torch.nn as nn -import torch.optim as optim -import warnings - -from rsl_rl.modules import ActorCritic -from rsl_rl.modules.rnd import RandomNetworkDistillation -from rsl_rl.utils import string_to_callable - -from ..storage.replay_storage import ReplayStorage -from ..storage.rollout_storage import RolloutStorage - - -class PPO: - """Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347).""" - - actor_critic: ActorCritic - """The actor critic module.""" - - def __init__( - self, - actor_critic, - num_learning_epochs=1, - num_mini_batches=1, - clip_param=0.2, - gamma=0.998, - lam=0.95, - value_loss_coef=1.0, - entropy_coef=0.0, - learning_rate=1e-3, - max_grad_norm=1.0, - use_clipped_value_loss=True, - schedule="fixed", - desired_kl=0.01, - device="cpu", - # BC parameters - behavior_cloning_cfg: dict | None = None, - # RND parameters - rnd_cfg: dict | None = None, - # Symmetry parameters - symmetry_cfg: dict | None = None, - # Offline configuration - offline_algorithm_cfg: dict | None = None, - ): - self.device = device - - self.desired_kl = desired_kl - self.schedule = schedule - self.learning_rate = learning_rate - - # Online configurations - # BC components - if behavior_cloning_cfg is not None: - self.bc = behavior_cloning_cfg - - if self.bc["experts_env_mapping_func"] is not None: - self.experts_env_id_map_fn = self.bc["expert_env_id_map_fn"] - else: - if len(behavior_cloning_cfg["experts_path"]) > 1: - raise ValueError("If you have multiple experts, you need to provide a mapping function.") - self.experts_env_id_map_fn = lambda expert_idx: slice(None) - - if self.bc["experts_observation_func"] is not None: - self.expert_obs_fn = self.bc["expert_obs_fn"] - else: - self.expert_obs_shape = None # same as student observation shape - self.expert_critic_obs_shape = None # same as student critic observation shape - - loader = self.bc["experts_loader"] - if not callable(loader): - loader = eval(loader) - self.experts = [loader(expert_path).to(self.device).eval() for expert_path in self.bc["experts_path"]] - - self.bc_loss_coeff = self.bc["cloning_loss_coeff"] - self.bc_decay = self.bc["loss_decay"] - self.learn_std = self.bc["learn_std"] - - else: - self.bc = None - self.experts = None - self.experts_env_id_map_fn = None - - # RND components - if rnd_cfg is not None: - # Create RND module - self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg) - # Create RND optimizer - params = self.rnd.predictor.parameters() - self.rnd_optimizer = optim.Adam(params, lr=rnd_cfg.get("learning_rate", 1e-3)) - else: - self.rnd = None - self.rnd_optimizer = None - - # Symmetry components - if symmetry_cfg is not None: - # Check if symmetry is enabled - use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"] - # Print that we are not using symmetry - if not use_symmetry: - warnings.warn("Symmetry not used for learning. We will use it for logging instead.") - # If function is a string then resolve it to a function - if isinstance(symmetry_cfg["data_augmentation_func"], str): - symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"]) - # Check valid configuration - if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]): - raise ValueError( - "Data augmentation enabled but the function is not callable:" - f" {symmetry_cfg['data_augmentation_func']}" - ) - # Store symmetry configuration - self.symmetry = symmetry_cfg - else: - self.symmetry = None - - # PPO components - self.actor_critic = actor_critic - self.actor_critic.to(self.device) - self.storage = None # initialized later - self.optimizer = optim.Adam(self.actor_critic.parameters(), lr=learning_rate) - self.transition = RolloutStorage.Transition() - - # PPO parameters - self.clip_param = clip_param - self.num_learning_epochs = num_learning_epochs - self.num_mini_batches = num_mini_batches - self.value_loss_coef = value_loss_coef - self.entropy_coef = entropy_coef - self.gamma = gamma - self.lam = lam - self.max_grad_norm = max_grad_norm - self.use_clipped_value_loss = use_clipped_value_loss - - # Offline configuration - if offline_algorithm_cfg is not None: - self.offline = True - self.offline_algorithm_cfg = offline_algorithm_cfg - - self.update_counter = 0 - self.update_frequency = self.offline_algorithm_cfg["update_frequencies"] - self.offline_batch_size: int | None = self.offline_algorithm_cfg["batch_size"] - self.offline_num_learning_epochs: int | None = self.offline_algorithm_cfg["num_learning_epochs"] - - # Offline BC - if "behavior_cloning_cfg" in self.offline_algorithm_cfg: - self.offline_bc = self.offline_algorithm_cfg["behavior_cloning_cfg"] - - if self.offline_bc["experts_env_mapping_func"] is not None: - self.offline_experts_env_id_map_fn = self.offline_bc["expert_env_id_map_fn"] - else: - if len(self.offline_bc["experts_path"]) > 1: - raise ValueError("If you have multiple experts, you need to provide a mapping function.") - self.offline_experts_env_id_map_fn = lambda expert_idx: slice(None) - - if self.offline_bc["experts_observation_func"] is not None: - import importlib - - mod, attr_name = self.offline_bc["experts_observation_func"].split(":") - func = getattr(importlib.import_module(mod), attr_name) - self.offline_expert_obs_fn = func - self.offline_expert_obs_shape = self.offline_expert_obs_fn(self.offline_bc["_env"]).shape[1] - else: - self.offline_expert_obs_fn = None - self.offline_expert_obs_shape = None # same as student observation shape - - loader = self.offline_bc["experts_loader"] - if not callable(loader): - loader = eval(loader) - self.offline_experts = [ - loader(expert_path).to(self.device).eval() for expert_path in self.offline_bc["experts_path"] - ] - - self.offline_bc_loss_coeff = self.offline_bc["cloning_loss_coeff"] - self.offline_bc_decay = self.offline_bc["loss_decay"] - self.offline_learn_std = self.offline_bc["learn_std"] - else: - self.offline_bc = False - else: - self.offline = False - self.offline_bc = False # needed the field to exist so can be evaluated with out hasattr(self, offline_bc) - - def init_storage(self, num_envs, num_transitions_per_env, actor_obs_shape, critic_obs_shape, action_shape): - # create memory for RND as well :) - if self.rnd: - rnd_state_shape = [self.rnd.num_states] - else: - rnd_state_shape = None - - expert_mean_action_shape, expert_std_action_shape = None, None - if self.bc: - expert_mean_action_shape = action_shape - expert_std_action_shape = action_shape if self.learn_std else None - if self.offline_bc: - expert_mean_action_shape = action_shape - expert_std_action_shape = action_shape if expert_std_action_shape or self.offline_learn_std else None - - # create rollout storage - self.storage = RolloutStorage( - num_envs, - num_transitions_per_env, - actor_obs_shape, - critic_obs_shape, - action_shape, - rnd_state_shape, - expert_mean_action_shape, - expert_std_action_shape, - self.device, - ) - # create replay storage - if self.offline: - if self.offline_bc: - expert_mean_action_shape = action_shape - expert_std_action_shape = action_shape if self.offline_learn_std else None - else: - expert_mean_action_shape, expert_std_action_shape = None, None - self.replay_storage = ReplayStorage( - num_envs, - num_transitions_per_env * 20, - actor_obs_shape, - critic_obs_shape, - action_shape, - rnd_state_shape, - expert_mean_action_shape, - expert_std_action_shape, - self.device, - ) - - def test_mode(self): - self.actor_critic.test() - - def train_mode(self): - self.actor_critic.train() - - def act(self, obs, critic_obs): - if self.actor_critic.is_recurrent: - self.transition.hidden_states = self.actor_critic.get_hidden_states() - # Compute the actions and values - self.transition.actions = self.actor_critic.act(obs).detach() - self.transition.values = self.actor_critic.evaluate(critic_obs).detach() - self.transition.actions_log_prob = self.actor_critic.get_actions_log_prob(self.transition.actions).detach() - self.transition.action_mean = self.actor_critic.action_mean.detach() - self.transition.action_sigma = self.actor_critic.action_std.detach() - # need to record obs and critic_obs before env.step() - self.transition.observations = obs - self.transition.critic_observations = critic_obs - # BC component - if self.bc: - for i in range(len(self.experts)): - idx_mask = self.experts_env_id_map_fn(i) - self.transition.expert_action_mean = self.experts[i](obs[idx_mask]) - if self.learn_std: - self.transition.expert_action_sigma = self.experts[i].get_actions_log_prob( - self.transition.expert_action_mean - ) - if self.offline_bc: - for i in range(len(self.offline_experts)): - idx_mask = self.offline_experts_env_id_map_fn(i) - expert_obs = obs - if self.offline_expert_obs_fn: - expert_obs = self.offline_expert_obs_fn(self.offline_bc["_env"]) - self.transition.expert_action_mean = self.offline_experts[i](expert_obs[idx_mask]) - if self.offline_learn_std: - self.transition.expert_action_sigma = self.offline_experts[i].get_actions_log_prob( - self.transition.expert_action_mean - ) - - return self.transition.actions - - def process_env_step(self, rewards, dones, infos): - # Record the rewards and dones - # Note: we clone here because later on we bootstrap the rewards based on timeouts - self.transition.rewards = rewards.clone() - self.transition.dones = dones - - # Compute the intrinsic rewards and add to extrinsic rewards - if self.rnd: - # Obtain curiosity gates / observations from infos - rnd_state = infos["observations"]["rnd_state"] - # Compute the intrinsic rewards - # note: rnd_state is the gated_state after normalization if normalization is used - self.intrinsic_rewards, rnd_state = self.rnd.get_intrinsic_reward(rnd_state) - # Add intrinsic rewards to extrinsic rewards - self.transition.rewards += self.intrinsic_rewards - # Record the curiosity gates - self.transition.rnd_state = rnd_state.clone() - - # Bootstrapping on time outs - if "time_outs" in infos: - self.transition.rewards += self.gamma * torch.squeeze( - self.transition.values * infos["time_outs"].unsqueeze(1).to(self.device), 1 - ) - - # Record the transition - self.storage.add_transitions(self.transition) - self.transition.clear() - self.actor_critic.reset(dones) - - def compute_returns(self, last_critic_obs): - # compute value for the last step - last_values = self.actor_critic.evaluate(last_critic_obs).detach() - self.storage.compute_returns(last_values, self.gamma, self.lam) - - def transfer_rollout_to_replay(self, fields: list[str] = ["observations", "privileged_observations"]): - num_steps = self.storage.step # number of valid transitions in rollout - if num_steps == 0: - return - - # Helper to perform vectorized copy into a circular buffer. - def copy_to_replay(replay_field: torch.Tensor, rollout_field: torch.Tensor): - # Detach and clone the slice from rollout storage. - # shape: [num_steps, num_envs, ...] - start_idx = self.replay_storage.step - end_idx = start_idx + num_steps - if end_idx <= self.replay_storage.capacity: - # No wrap-around needed. - replay_field[start_idx:end_idx] = rollout_field[:num_steps].detach().clone() - else: - # Wrap-around: split the batch copy into two parts. - part1 = self.replay_storage.capacity - start_idx - replay_field[start_idx:] = rollout_field[:part1].detach().clone() - replay_field[: end_idx % self.replay_storage.capacity] = rollout_field[part1:].detach().clone() - - # Iterate over the specified fields and transfer them if available. - for field in fields: - copy_to_replay(getattr(self.replay_storage, field), getattr(self.storage, field)) - - # Update circular buffer pointers in replay storage. - self.replay_storage.step = (self.replay_storage.step + num_steps) % self.replay_storage.capacity - self.replay_storage.size = min(self.replay_storage.size + num_steps, self.replay_storage.capacity) - - def update(self): # noqa: C901 - mean_value_loss = 0 - mean_surrogate_loss = 0 - mean_entropy = 0 - # -- BC loss - if self.bc: - mean_bc_loss = 0 - else: - mean_bc_loss = None - # -- RND loss - if self.rnd: - mean_rnd_loss = 0 - else: - mean_rnd_loss = None - # -- Symmetry loss - if self.symmetry: - mean_symmetry_loss = 0 - else: - mean_symmetry_loss = None - - # generator for mini batches - if self.actor_critic.is_recurrent: - generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs) - else: - generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs) - # iterate over batches - for ( - obs_batch, - critic_obs_batch, - actions_batch, - target_values_batch, - advantages_batch, - returns_batch, - old_actions_log_prob_batch, - old_mu_batch, - old_sigma_batch, - hid_states_batch, - masks_batch, - expert_action_mu_batch, - expert_action_sigma_batch, - rnd_state_batch, - ) in generator: - - # number of augmentations per sample - # we start with 1 and increase it if we use symmetry augmentation - num_aug = 1 - # original batch size - original_batch_size = obs_batch.shape[0] - - # Perform symmetric augmentation - if self.symmetry and self.symmetry["use_data_augmentation"]: - # augmentation using symmetry - data_augmentation_func = self.symmetry["data_augmentation_func"] - # returned shape: [batch_size * num_aug, ...] - obs_batch, actions_batch = data_augmentation_func( - obs=obs_batch, actions=actions_batch, env=self.symmetry["_env"], is_critic=False - ) - critic_obs_batch, _ = data_augmentation_func( - obs=critic_obs_batch, actions=None, env=self.symmetry["_env"], is_critic=True - ) - # compute number of augmentations per sample - num_aug = int(obs_batch.shape[0] / original_batch_size) - # repeat the rest of the batch - # -- actor - old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1) - # -- critic - target_values_batch = target_values_batch.repeat(num_aug, 1) - advantages_batch = advantages_batch.repeat(num_aug, 1) - returns_batch = returns_batch.repeat(num_aug, 1) - - # Recompute actions log prob and entropy for current batch of transitions - # Note: we need to do this because we updated the actor_critic with the new parameters - # -- actor - self.actor_critic.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0]) - actions_log_prob_batch = self.actor_critic.get_actions_log_prob(actions_batch) - # -- critic - value_batch = self.actor_critic.evaluate( - critic_obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1] - ) - # -- entropy - # we only keep the entropy of the first augmentation (the original one) - mu_batch = self.actor_critic.action_mean[:original_batch_size] - sigma_batch = self.actor_critic.action_std[:original_batch_size] - entropy_batch = self.actor_critic.entropy[:original_batch_size] - - # KL - if self.desired_kl is not None and self.schedule == "adaptive": - with torch.inference_mode(): - kl = torch.sum( - torch.log(sigma_batch / old_sigma_batch + 1.0e-5) - + (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch)) - / (2.0 * torch.square(sigma_batch)) - - 0.5, - axis=-1, - ) - kl_mean = torch.mean(kl) - - if kl_mean > self.desired_kl * 2.0: - self.learning_rate = max(1e-5, self.learning_rate / 1.5) - elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0: - self.learning_rate = min(1e-2, self.learning_rate * 1.5) - - for param_group in self.optimizer.param_groups: - param_group["lr"] = self.learning_rate - - # Surrogate loss - ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch)) - surrogate = -torch.squeeze(advantages_batch) * ratio - surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp( - ratio, 1.0 - self.clip_param, 1.0 + self.clip_param - ) - surrogate_loss = torch.max(surrogate, surrogate_clipped).mean() - - # Value function loss - if self.use_clipped_value_loss: - value_clipped = target_values_batch + (value_batch - target_values_batch).clamp( - -self.clip_param, self.clip_param - ) - value_losses = (value_batch - returns_batch).pow(2) - value_losses_clipped = (value_clipped - returns_batch).pow(2) - value_loss = torch.max(value_losses, value_losses_clipped).mean() - else: - value_loss = (returns_batch - value_batch).pow(2).mean() - - loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean() - - # Symmetry loss - if self.symmetry: - # obtain the symmetric actions - # if we did augmentation before then we don't need to augment again - if not self.symmetry["use_data_augmentation"]: - data_augmentation_func = self.symmetry["data_augmentation_func"] - obs_batch, _ = data_augmentation_func( - obs=obs_batch, actions=None, env=self.symmetry["_env"], is_critic=False - ) - # compute number of augmentations per sample - num_aug = int(obs_batch.shape[0] / original_batch_size) - - # actions predicted by the actor for symmetrically-augmented observations - mean_actions_batch = self.actor_critic.act_inference(obs_batch.detach().clone()) - - # compute the symmetrically augmented actions - # note: we are assuming the first augmentation is the original one. - # We do not use the action_batch from earlier since that action was sampled from the distribution. - # However, the symmetry loss is computed using the mean of the distribution. - action_mean_orig = mean_actions_batch[:original_batch_size] - _, actions_mean_symm_batch = data_augmentation_func( - obs=None, actions=action_mean_orig, env=self.symmetry["_env"], is_critic=False - ) - - # compute the loss (we skip the first augmentation as it is the original one) - mse_loss = torch.nn.MSELoss() - symmetry_loss = mse_loss( - mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:] - ) - # add the loss to the total loss - if self.symmetry["use_mirror_loss"]: - loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss - else: - symmetry_loss = symmetry_loss.detach() - # BC loss - if self.bc: - mse_loss = torch.nn.MSELoss() - mean_loss = mse_loss(mu_batch, expert_action_mu_batch) - bc_loss = mean_loss - if self.learn_std: - std_loss = mse_loss(sigma_batch, expert_action_sigma_batch) - bc_loss += std_loss - self.bc_loss_coeff *= self.bc_decay - loss = (1 - self.bc_loss_coeff) * loss + self.bc_loss_coeff * bc_loss - - # Random Network Distillation loss - if self.rnd: - # predict the embedding and the target - predicted_embedding = self.rnd.predictor(rnd_state_batch) - target_embedding = self.rnd.target(rnd_state_batch) - # compute the loss as the mean squared error - mseloss = torch.nn.MSELoss() - rnd_loss = mseloss(predicted_embedding, target_embedding.detach()) - - # Gradient step - # -- For PPO - self.optimizer.zero_grad() - loss.backward() - nn.utils.clip_grad_norm_(self.actor_critic.parameters(), self.max_grad_norm) - self.optimizer.step() - # -- For RND - if self.rnd_optimizer: - self.rnd_optimizer.zero_grad() - rnd_loss.backward() - self.rnd_optimizer.step() - - # Store the losses - mean_value_loss += value_loss.item() - mean_surrogate_loss += surrogate_loss.item() - mean_entropy += entropy_batch.mean().item() - # -- BC loss - if mean_bc_loss is not None: - mean_bc_loss += bc_loss.item() - # -- RND loss - if mean_rnd_loss is not None: - mean_rnd_loss += rnd_loss.item() - # -- Symmetry loss - if mean_symmetry_loss is not None: - mean_symmetry_loss += symmetry_loss.item() - - # -- For PPO - num_updates = self.num_learning_epochs * self.num_mini_batches - mean_value_loss /= num_updates - mean_surrogate_loss /= num_updates - # -- For BC - if mean_bc_loss is not None: - mean_bc_loss /= num_updates - # -- For RND - if mean_rnd_loss is not None: - mean_rnd_loss /= num_updates - # -- For Symmetry - if mean_symmetry_loss is not None: - mean_symmetry_loss /= num_updates - # -- Clear the storage - - if self.offline: - fields_to_transfer = ["observations", "privileged_observations"] - if self.offline_bc: - fields_to_transfer.append("expert_action_mean") - if self.offline_learn_std: - fields_to_transfer.append("expert_action_sigma") - self.transfer_rollout_to_replay(fields_to_transfer) - while self.update_counter >= 0: - capacity_percentage, offline_bc_loss = self.update_offline() - self.update_counter -= 1 / self.update_frequency - self.update_counter += 1 - else: - capacity_percentage, offline_bc_loss = None, None - self.storage.clear() - - return ( - mean_value_loss, - mean_surrogate_loss, - mean_entropy, - mean_bc_loss, - mean_rnd_loss, - mean_symmetry_loss, - capacity_percentage, - offline_bc_loss, - ) - - def update_offline(self): - loss = 0 - storage_data_size = self.replay_storage.num_envs * self.replay_storage.size - - batch_size = ( - self.offline_batch_size - if self.offline_batch_size - else self.storage.num_envs * self.storage.num_transitions_per_env // self.num_mini_batches - ) - num_mini_batches = storage_data_size // batch_size - num_learning_epoch = ( - self.offline_num_learning_epochs if self.offline_num_learning_epochs else self.num_learning_epochs - ) - - if self.actor_critic.is_recurrent: - generator = self.replay_storage.recurrent_mini_batch_generator(num_mini_batches, num_learning_epoch) - else: - generator = self.replay_storage.mini_batch_generator(num_mini_batches, num_learning_epoch) - - # -- For BC - if self.offline_bc: - mse_loss_fn = nn.MSELoss() - mean_bc_loss = 0.0 - else: - mean_bc_loss = None - - for obs_batch, _, _, _, _, _, _, _, _, _, _, expert_action_mu_batch, expert_action_sigma_batch, _ in generator: - # -- For BC - if self.offline_bc: - predicted_actions = self.actor_critic.act_inference(obs_batch) - bc_loss = mse_loss_fn(predicted_actions, expert_action_mu_batch) - if self.offline_learn_std: - predicted_std = self.actor_critic.action_std[: obs_batch.shape[0]] - bc_loss += mse_loss_fn(predicted_std, expert_action_sigma_batch) - self.offline_bc_loss_coeff *= self.offline_bc_decay - loss = (1 - self.offline_bc_loss_coeff) * loss + self.offline_bc_loss_coeff * bc_loss - - self.optimizer.zero_grad() - bc_loss.backward() - nn.utils.clip_grad_norm_(self.actor_critic.parameters(), self.max_grad_norm) - self.optimizer.step() - - # --BC loss - if mean_bc_loss is not None: - mean_bc_loss += bc_loss.item() - - num_updates = num_mini_batches * num_learning_epoch - # For BC - if mean_bc_loss is not None: - mean_bc_loss /= num_updates - capacity_percentage = self.replay_storage.size / self.replay_storage.capacity - return capacity_percentage, mean_bc_loss diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/original_ppo.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/original_ppo.py deleted file mode 100644 index 8b3ec74..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/algorithms/original_ppo.py +++ /dev/null @@ -1,378 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import torch -import torch.nn as nn -import torch.optim as optim -import warnings - -from rsl_rl.modules import ActorCritic -from rsl_rl.modules.rnd import RandomNetworkDistillation -from rsl_rl.storage import RolloutStorage -from rsl_rl.utils import string_to_callable - - -class PPO: - """Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347).""" - - actor_critic: ActorCritic - """The actor critic module.""" - - def __init__( - self, - actor_critic, - num_learning_epochs=1, - num_mini_batches=1, - clip_param=0.2, - gamma=0.998, - lam=0.95, - value_loss_coef=1.0, - entropy_coef=0.0, - learning_rate=1e-3, - max_grad_norm=1.0, - use_clipped_value_loss=True, - schedule="fixed", - desired_kl=0.01, - device="cpu", - # RND parameters - rnd_cfg: dict | None = None, - # Symmetry parameters - symmetry_cfg: dict | None = None, - ): - self.device = device - - self.desired_kl = desired_kl - self.schedule = schedule - self.learning_rate = learning_rate - - # RND components - if rnd_cfg is not None: - # Create RND module - self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg) - # Create RND optimizer - params = self.rnd.predictor.parameters() - self.rnd_optimizer = optim.Adam(params, lr=rnd_cfg.get("learning_rate", 1e-3)) - else: - self.rnd = None - self.rnd_optimizer = None - - # Symmetry components - if symmetry_cfg is not None: - # Check if symmetry is enabled - use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"] - # Print that we are not using symmetry - if not use_symmetry: - warnings.warn("Symmetry not used for learning. We will use it for logging instead.") - # If function is a string then resolve it to a function - if isinstance(symmetry_cfg["data_augmentation_func"], str): - symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"]) - # Check valid configuration - if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]): - raise ValueError( - "Data augmentation enabled but the function is not callable:" - f" {symmetry_cfg['data_augmentation_func']}" - ) - # Store symmetry configuration - self.symmetry = symmetry_cfg - else: - self.symmetry = None - - # PPO components - self.actor_critic = actor_critic - self.actor_critic.to(self.device) - self.storage = None # initialized later - self.optimizer = optim.Adam(self.actor_critic.parameters(), lr=learning_rate) - self.transition = RolloutStorage.Transition() - - # PPO parameters - self.clip_param = clip_param - self.num_learning_epochs = num_learning_epochs - self.num_mini_batches = num_mini_batches - self.value_loss_coef = value_loss_coef - self.entropy_coef = entropy_coef - self.gamma = gamma - self.lam = lam - self.max_grad_norm = max_grad_norm - self.use_clipped_value_loss = use_clipped_value_loss - - def init_storage(self, num_envs, num_transitions_per_env, actor_obs_shape, critic_obs_shape, action_shape): - # create memory for RND as well :) - if self.rnd: - rnd_state_shape = [self.rnd.num_states] - else: - rnd_state_shape = None - # create rollout storage - self.storage = RolloutStorage( - num_envs, - num_transitions_per_env, - actor_obs_shape, - critic_obs_shape, - action_shape, - rnd_state_shape, - self.device, - ) - - def test_mode(self): - self.actor_critic.test() - - def train_mode(self): - self.actor_critic.train() - - def act(self, obs, critic_obs): - if self.actor_critic.is_recurrent: - self.transition.hidden_states = self.actor_critic.get_hidden_states() - # Compute the actions and values - self.transition.actions = self.actor_critic.act(obs).detach() - self.transition.values = self.actor_critic.evaluate(critic_obs).detach() - self.transition.actions_log_prob = self.actor_critic.get_actions_log_prob(self.transition.actions).detach() - self.transition.action_mean = self.actor_critic.action_mean.detach() - self.transition.action_sigma = self.actor_critic.action_std.detach() - # need to record obs and critic_obs before env.step() - self.transition.observations = obs - self.transition.critic_observations = critic_obs - return self.transition.actions - - def process_env_step(self, rewards, dones, infos): - # Record the rewards and dones - # Note: we clone here because later on we bootstrap the rewards based on timeouts - self.transition.rewards = rewards.clone() - self.transition.dones = dones - - # Compute the intrinsic rewards and add to extrinsic rewards - if self.rnd: - # Obtain curiosity gates / observations from infos - rnd_state = infos["observations"]["rnd_state"] - # Compute the intrinsic rewards - # note: rnd_state is the gated_state after normalization if normalization is used - self.intrinsic_rewards, rnd_state = self.rnd.get_intrinsic_reward(rnd_state) - # Add intrinsic rewards to extrinsic rewards - self.transition.rewards += self.intrinsic_rewards - # Record the curiosity gates - self.transition.rnd_state = rnd_state.clone() - - # Bootstrapping on time outs - if "time_outs" in infos: - self.transition.rewards += self.gamma * torch.squeeze( - self.transition.values * infos["time_outs"].unsqueeze(1).to(self.device), 1 - ) - - # Record the transition - self.storage.add_transitions(self.transition) - self.transition.clear() - self.actor_critic.reset(dones) - - def compute_returns(self, last_critic_obs): - # compute value for the last step - last_values = self.actor_critic.evaluate(last_critic_obs).detach() - self.storage.compute_returns(last_values, self.gamma, self.lam) - - def update(self): # noqa: C901 - mean_value_loss = 0 - mean_surrogate_loss = 0 - mean_entropy = 0 - # -- RND loss - if self.rnd: - mean_rnd_loss = 0 - else: - mean_rnd_loss = None - # -- Symmetry loss - if self.symmetry: - mean_symmetry_loss = 0 - else: - mean_symmetry_loss = None - - # generator for mini batches - if self.actor_critic.is_recurrent: - generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs) - else: - generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs) - - # iterate over batches - for ( - obs_batch, - critic_obs_batch, - actions_batch, - target_values_batch, - advantages_batch, - returns_batch, - old_actions_log_prob_batch, - old_mu_batch, - old_sigma_batch, - hid_states_batch, - masks_batch, - rnd_state_batch, - ) in generator: - - # number of augmentations per sample - # we start with 1 and increase it if we use symmetry augmentation - num_aug = 1 - # original batch size - original_batch_size = obs_batch.shape[0] - - # Perform symmetric augmentation - if self.symmetry and self.symmetry["use_data_augmentation"]: - # augmentation using symmetry - data_augmentation_func = self.symmetry["data_augmentation_func"] - # returned shape: [batch_size * num_aug, ...] - obs_batch, actions_batch = data_augmentation_func( - obs=obs_batch, actions=actions_batch, env=self.symmetry["_env"], is_critic=False - ) - critic_obs_batch, _ = data_augmentation_func( - obs=critic_obs_batch, actions=None, env=self.symmetry["_env"], is_critic=True - ) - # compute number of augmentations per sample - num_aug = int(obs_batch.shape[0] / original_batch_size) - # repeat the rest of the batch - # -- actor - old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1) - # -- critic - target_values_batch = target_values_batch.repeat(num_aug, 1) - advantages_batch = advantages_batch.repeat(num_aug, 1) - returns_batch = returns_batch.repeat(num_aug, 1) - - # Recompute actions log prob and entropy for current batch of transitions - # Note: we need to do this because we updated the actor_critic with the new parameters - # -- actor - self.actor_critic.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0]) - actions_log_prob_batch = self.actor_critic.get_actions_log_prob(actions_batch) - # -- critic - value_batch = self.actor_critic.evaluate( - critic_obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1] - ) - # -- entropy - # we only keep the entropy of the first augmentation (the original one) - mu_batch = self.actor_critic.action_mean[:original_batch_size] - sigma_batch = self.actor_critic.action_std[:original_batch_size] - entropy_batch = self.actor_critic.entropy[:original_batch_size] - - # KL - if self.desired_kl is not None and self.schedule == "adaptive": - with torch.inference_mode(): - kl = torch.sum( - torch.log(sigma_batch / old_sigma_batch + 1.0e-5) - + (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch)) - / (2.0 * torch.square(sigma_batch)) - - 0.5, - axis=-1, - ) - kl_mean = torch.mean(kl) - - if kl_mean > self.desired_kl * 2.0: - self.learning_rate = max(1e-5, self.learning_rate / 1.5) - elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0: - self.learning_rate = min(1e-2, self.learning_rate * 1.5) - - for param_group in self.optimizer.param_groups: - param_group["lr"] = self.learning_rate - - # Surrogate loss - ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch)) - surrogate = -torch.squeeze(advantages_batch) * ratio - surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp( - ratio, 1.0 - self.clip_param, 1.0 + self.clip_param - ) - surrogate_loss = torch.max(surrogate, surrogate_clipped).mean() - - # Value function loss - if self.use_clipped_value_loss: - value_clipped = target_values_batch + (value_batch - target_values_batch).clamp( - -self.clip_param, self.clip_param - ) - value_losses = (value_batch - returns_batch).pow(2) - value_losses_clipped = (value_clipped - returns_batch).pow(2) - value_loss = torch.max(value_losses, value_losses_clipped).mean() - else: - value_loss = (returns_batch - value_batch).pow(2).mean() - - loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean() - - # Symmetry loss - if self.symmetry: - # obtain the symmetric actions - # if we did augmentation before then we don't need to augment again - if not self.symmetry["use_data_augmentation"]: - data_augmentation_func = self.symmetry["data_augmentation_func"] - obs_batch, _ = data_augmentation_func( - obs=obs_batch, actions=None, env=self.symmetry["_env"], is_critic=False - ) - # compute number of augmentations per sample - num_aug = int(obs_batch.shape[0] / original_batch_size) - - # actions predicted by the actor for symmetrically-augmented observations - mean_actions_batch = self.actor_critic.act_inference(obs_batch.detach().clone()) - - # compute the symmetrically augmented actions - # note: we are assuming the first augmentation is the original one. - # We do not use the action_batch from earlier since that action was sampled from the distribution. - # However, the symmetry loss is computed using the mean of the distribution. - action_mean_orig = mean_actions_batch[:original_batch_size] - _, actions_mean_symm_batch = data_augmentation_func( - obs=None, actions=action_mean_orig, env=self.symmetry["_env"], is_critic=False - ) - - # compute the loss (we skip the first augmentation as it is the original one) - mse_loss = torch.nn.MSELoss() - symmetry_loss = mse_loss( - mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:] - ) - # add the loss to the total loss - if self.symmetry["use_mirror_loss"]: - loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss - else: - symmetry_loss = symmetry_loss.detach() - - # Random Network Distillation loss - if self.rnd: - # predict the embedding and the target - predicted_embedding = self.rnd.predictor(rnd_state_batch) - target_embedding = self.rnd.target(rnd_state_batch) - # compute the loss as the mean squared error - mseloss = torch.nn.MSELoss() - rnd_loss = mseloss(predicted_embedding, target_embedding.detach()) - - # Gradient step - # -- For PPO - self.optimizer.zero_grad() - loss.backward() - nn.utils.clip_grad_norm_(self.actor_critic.parameters(), self.max_grad_norm) - self.optimizer.step() - # -- For RND - if self.rnd_optimizer: - self.rnd_optimizer.zero_grad() - rnd_loss.backward() - self.rnd_optimizer.step() - - # Store the losses - mean_value_loss += value_loss.item() - mean_surrogate_loss += surrogate_loss.item() - mean_entropy += entropy_batch.mean().item() - # -- RND loss - if mean_rnd_loss is not None: - mean_rnd_loss += rnd_loss.item() - # -- Symmetry loss - if mean_symmetry_loss is not None: - mean_symmetry_loss += symmetry_loss.item() - - # -- For PPO - num_updates = self.num_learning_epochs * self.num_mini_batches - mean_value_loss /= num_updates - mean_surrogate_loss /= num_updates - # -- For RND - if mean_rnd_loss is not None: - mean_rnd_loss /= num_updates - # -- For Symmetry - if mean_symmetry_loss is not None: - mean_symmetry_loss /= num_updates - # -- Clear the storage - self.storage.clear() - - return mean_value_loss, mean_surrogate_loss, mean_entropy, mean_rnd_loss, mean_symmetry_loss diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/runners/on_policy_runner.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/runners/on_policy_runner.py deleted file mode 100644 index b0335d9..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/runners/on_policy_runner.py +++ /dev/null @@ -1,472 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import os -import statistics -import time -import torch -from collections import deque - -import rsl_rl -from rsl_rl.env import VecEnv -from rsl_rl.modules import ActorCritic, ActorCriticRecurrent, EmpiricalNormalization -from rsl_rl.utils import store_code_state - -from ..algorithms.extensible_ppo import PPO - - -class OnPolicyRunner: - """On-policy runner for training and evaluation.""" - - def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"): - self.cfg = train_cfg - self.alg_cfg = train_cfg["algorithm"] - self.policy_cfg = train_cfg["policy"] - self.device = device - self.env = env - - # resolve dimensions of observations - obs, extras = self.env.get_observations() - num_obs = obs.shape[1] - if "critic" in extras["observations"]: - num_critic_obs = extras["observations"]["critic"].shape[1] - else: - num_critic_obs = num_obs - actor_critic_class = eval(self.policy_cfg.pop("class_name")) # ActorCritic - actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class( - num_obs, num_critic_obs, self.env.num_actions, **self.policy_cfg - ).to(self.device) - - # resolve dimension of rnd gated state - if "rnd_cfg" in self.alg_cfg: - # check if rnd gated state is present - rnd_state = extras["observations"].get("rnd_state") - if rnd_state is None: - raise ValueError("Observations for they key 'rnd_state' not found in infos['observations'].") - # get dimension of rnd gated state - num_rnd_state = rnd_state.shape[1] - # add rnd gated state to config - self.alg_cfg["rnd_cfg"]["num_state"] = num_rnd_state - # scale down the rnd weight with timestep (similar to how rewards are scaled down in legged_gym envs) - self.alg_cfg["rnd_cfg"]["weight"] *= env.dt - - # if using symmetry then pass the environment config object - if "symmetry_cfg" in self.alg_cfg: - # this is used by the symmetry function for handling different observation terms - self.alg_cfg["symmetry_cfg"]["_env"] = env - - if "offline_algorithm_cfg" in self.alg_cfg: - if "behavior_cloning_cfg" in self.alg_cfg["offline_algorithm_cfg"]: - # this is used by the symmetry function for handling different observation terms - self.alg_cfg["offline_algorithm_cfg"]["behavior_cloning_cfg"]["_env"] = env - - # init algorithm - alg_class = eval(self.alg_cfg.pop("class_name")) # PPO - self.alg: PPO = alg_class(actor_critic, device=self.device, **self.alg_cfg) - - # store training configuration - self.num_steps_per_env = self.cfg["num_steps_per_env"] - self.save_interval = self.cfg["save_interval"] - self.empirical_normalization = self.cfg["empirical_normalization"] - if self.empirical_normalization: - self.obs_normalizer = EmpiricalNormalization(shape=[num_obs], until=1.0e8).to(self.device) - self.critic_obs_normalizer = EmpiricalNormalization(shape=[num_critic_obs], until=1.0e8).to(self.device) - else: - self.obs_normalizer = torch.nn.Identity().to(self.device) # no normalization - self.critic_obs_normalizer = torch.nn.Identity().to(self.device) # no normalization - # init storage and model - self.alg.init_storage( - self.env.num_envs, - self.num_steps_per_env, - [num_obs], - [num_critic_obs], - [self.env.num_actions], - ) - - # Log - self.log_dir = log_dir - self.writer = None - self.tot_timesteps = 0 - self.tot_time = 0 - self.current_learning_iteration = 0 - self.git_status_repos = [rsl_rl.__file__] - - def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): - # initialize writer - if self.log_dir is not None and self.writer is None: - # Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard. - self.logger_type = self.cfg.get("logger", "tensorboard") - self.logger_type = self.logger_type.lower() - - if self.logger_type == "neptune": - from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter - - self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg) - self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg) - elif self.logger_type == "wandb": - from rsl_rl.utils.wandb_utils import WandbSummaryWriter - - self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg) - self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg) - elif self.logger_type == "tensorboard": - from torch.utils.tensorboard import SummaryWriter - - self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10) - else: - raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.") - - # randomize initial episode lengths (for exploration) - if init_at_random_ep_len: - self.env.episode_length_buf = torch.randint_like( - self.env.episode_length_buf, high=int(self.env.max_episode_length) - ) - - # start learning - obs, extras = self.env.get_observations() - critic_obs = extras["observations"].get("critic", obs) - obs, critic_obs = obs.to(self.device), critic_obs.to(self.device) - self.train_mode() # switch to train mode (for dropout for example) - - # Book keeping - ep_infos = [] - rewbuffer = deque(maxlen=100) - lenbuffer = deque(maxlen=100) - cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - # create buffers for logging extrinsic and intrinsic rewards - if self.alg.rnd: - erewbuffer = deque(maxlen=100) - irewbuffer = deque(maxlen=100) - cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - - start_iter = self.current_learning_iteration - tot_iter = start_iter + num_learning_iterations - for it in range(start_iter, tot_iter): - start = time.time() - # Rollout - with torch.inference_mode(): - for _ in range(self.num_steps_per_env): - # Sample actions from policy - actions = self.alg.act(obs, critic_obs) - # Step environment - obs, rewards, dones, infos = self.env.step(actions.to(self.env.device)) - - # Move to the agent device - obs, rewards, dones = obs.to(self.device), rewards.to(self.device), dones.to(self.device) - - # Normalize observations - obs = self.obs_normalizer(obs) - # Extract critic observations and normalize - if "critic" in infos["observations"]: - critic_obs = self.critic_obs_normalizer(infos["observations"]["critic"].to(self.device)) - else: - critic_obs = obs - - # Intrinsic rewards (extracted here only for logging)! - intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None - - # Process env step and store in buffer - self.alg.process_env_step(rewards, dones, infos) - - if self.log_dir is not None: - # Book keeping - if "episode" in infos: - ep_infos.append(infos["episode"]) - elif "log" in infos: - ep_infos.append(infos["log"]) - # Update rewards - if self.alg.rnd: - cur_ereward_sum += rewards - cur_ireward_sum += intrinsic_rewards # type: ignore - cur_reward_sum += rewards + intrinsic_rewards - else: - cur_reward_sum += rewards - # Update episode length - cur_episode_length += 1 - # Clear data for completed episodes - # -- common - new_ids = (dones > 0).nonzero(as_tuple=False) - rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist()) - lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist()) - cur_reward_sum[new_ids] = 0 - cur_episode_length[new_ids] = 0 - # -- intrinsic and extrinsic rewards - if self.alg.rnd: - erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist()) - irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist()) - cur_ereward_sum[new_ids] = 0 - cur_ireward_sum[new_ids] = 0 - - stop = time.time() - collection_time = stop - start - - # Learning step - start = stop - self.alg.compute_returns(critic_obs) - - # Update policy - # Note: we keep arguments here since locals() loads them - ( - mean_value_loss, - mean_surrogate_loss, - mean_entropy, - mean_bc_loss, - mean_rnd_loss, - mean_symmetry_loss, - capacity_percentage, - offline_bc_loss, - ) = self.alg.update() - stop = time.time() - learn_time = stop - start - self.current_learning_iteration = it - - # Logging info and save checkpoint - if self.log_dir is not None: - # Log information - self.log(locals()) - # Save model - if it % self.save_interval == 0: - self.save(os.path.join(self.log_dir, f"model_{it}.pt")) - - # Clear episode infos - ep_infos.clear() - - # Save code state - if it == start_iter: - # obtain all the diff files - git_file_paths = store_code_state(self.log_dir, self.git_status_repos) - # if possible store them to wandb - if self.logger_type in ["wandb", "neptune"] and git_file_paths: - for path in git_file_paths: - self.writer.save_file(path) - - # Save the final model after training - if self.log_dir is not None: - self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt")) - - def log(self, locs: dict, width: int = 80, pad: int = 35): - self.tot_timesteps += self.num_steps_per_env * self.env.num_envs - self.tot_time += locs["collection_time"] + locs["learn_time"] - iteration_time = locs["collection_time"] + locs["learn_time"] - - # -- Episode info - ep_string = "" - if locs["ep_infos"]: - for key in locs["ep_infos"][0]: - infotensor = torch.tensor([], device=self.device) - for ep_info in locs["ep_infos"]: - # handle scalar and zero dimensional tensor infos - if key not in ep_info: - continue - if not isinstance(ep_info[key], torch.Tensor): - ep_info[key] = torch.Tensor([ep_info[key]]) - if len(ep_info[key].shape) == 0: - ep_info[key] = ep_info[key].unsqueeze(0) - infotensor = torch.cat((infotensor, ep_info[key].to(self.device))) - value = torch.mean(infotensor) - # log to logger and terminal - if "/" in key: - self.writer.add_scalar(key, value, locs["it"]) - ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n""" - else: - self.writer.add_scalar("Episode/" + key, value, locs["it"]) - ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n""" - mean_std = self.alg.actor_critic.std.mean() - fps = int(self.num_steps_per_env * self.env.num_envs / (locs["collection_time"] + locs["learn_time"])) - - # -- Losses - self.writer.add_scalar("Loss/value_function", locs["mean_value_loss"], locs["it"]) - self.writer.add_scalar("Loss/surrogate", locs["mean_surrogate_loss"], locs["it"]) - self.writer.add_scalar("Loss/entropy", locs["mean_entropy"], locs["it"]) - self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"]) - if self.alg.bc: - self.writer.add_scalar("Loss/bc", locs["mean_bc_loss"], locs["it"]) - if self.alg.rnd: - self.writer.add_scalar("Loss/rnd", locs["mean_rnd_loss"], locs["it"]) - if self.alg.symmetry: - self.writer.add_scalar("Loss/symmetry", locs["mean_symmetry_loss"], locs["it"]) - if self.alg.offline: - if self.alg.offline_bc: - self.writer.add_scalar("Loss/offline_bc", locs["offline_bc_loss"], locs["it"]) - - # -- Policy - self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"]) - - # -- Performance - self.writer.add_scalar("Perf/total_fps", fps, locs["it"]) - self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"]) - self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"]) - - # -- Training - if len(locs["rewbuffer"]) > 0: - # separate logging for intrinsic and extrinsic rewards - if self.alg.rnd: - self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"]) - self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"]) - self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"]) - # everything else - self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"]) - self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"]) - if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging - self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time) - self.writer.add_scalar( - "Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time - ) - - str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m " - - if len(locs["rewbuffer"]) > 0: - log_string = ( - f"""{'#' * width}\n""" - f"""{str.center(width, ' ')}\n\n""" - f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[ - 'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n""" - f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n""" - f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n""" - ) - - # -- For BC - if self.alg.bc: - log_string += f"""{'Behavior cloning loss:':>{pad}} {locs['mean_bc_loss']:.4f}\n""" - - # -- For symmetry - if self.alg.symmetry: - log_string += f"""{'Symmetry loss:':>{pad}} {locs['mean_symmetry_loss']:.4f}\n""" - - log_string += f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n""" - - # -- For RND - if self.alg.rnd: - log_string += ( - f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n""" - f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n""" - ) - - # -- For Offline - if self.alg.offline: - log_string += f"""{'Replay Capacity:':>{pad}} {locs['capacity_percentage']:.4f}\n""" - if self.alg.offline_bc: - log_string += f"""{'Offline BC loss:':>{pad}} {locs['offline_bc_loss']:.4f}\n""" - - log_string += f"""{'Mean total reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n""" - log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n""" - # f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n""" - # f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""") - else: - log_string = ( - f"""{'#' * width}\n""" - f"""{str.center(width, ' ')}\n\n""" - f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[ - 'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n""" - f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n""" - f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n""" - ) - # -- For symmetry - if self.alg.symmetry: - log_string += f"""{'Symmetry loss:':>{pad}} {locs['mean_symmetry_loss']:.4f}\n""" - - log_string += f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n""" - - # f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n""" - # f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""") - - log_string += ep_string - log_string += ( - f"""{'-' * width}\n""" - f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n""" - f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n""" - f"""{'Total time:':>{pad}} {self.tot_time:.2f}s\n""" - f"""{'ETA:':>{pad}} {self.tot_time / (locs['it'] + 1) * ( - locs['num_learning_iterations'] - locs['it']):.1f}s\n""" - ) - print(log_string) - - def save(self, path: str, infos=None): - # -- Save PPO model - saved_dict = { - "model_state_dict": self.alg.actor_critic.state_dict(), - "optimizer_state_dict": self.alg.optimizer.state_dict(), - "iter": self.current_learning_iteration, - "infos": infos, - } - # -- Save RND model if used - if self.alg.rnd: - saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict() - saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict() - # -- Save observation normalizer if used - if self.empirical_normalization: - saved_dict["obs_norm_state_dict"] = self.obs_normalizer.state_dict() - saved_dict["critic_obs_norm_state_dict"] = self.critic_obs_normalizer.state_dict() - torch.save(saved_dict, path) - - # Upload model to external logging service - if self.logger_type in ["neptune", "wandb"]: - self.writer.save_model(path, self.current_learning_iteration) - - def load(self, path: str, load_optimizer: bool = True): - loaded_dict = torch.load(path, weights_only=False) - # -- Load PPO model - self.alg.actor_critic.load_state_dict(loaded_dict["model_state_dict"]) - # -- Load RND model if used - if self.alg.rnd: - self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"]) - # -- Load observation normalizer if used - if self.empirical_normalization: - self.obs_normalizer.load_state_dict(loaded_dict["obs_norm_state_dict"]) - self.critic_obs_normalizer.load_state_dict(loaded_dict["critic_obs_norm_state_dict"]) - # -- Load optimizer if used - if load_optimizer: - # -- PPO - self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"]) - # -- RND optimizer if used - if self.alg.rnd: - self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"]) - # -- Load current learning iteration - self.current_learning_iteration = loaded_dict["iter"] - return loaded_dict["infos"] - - def get_inference_policy(self, device=None): - self.eval_mode() # switch to evaluation mode (dropout for example) - if device is not None: - self.alg.actor_critic.to(device) - policy = self.alg.actor_critic.act_inference - if self.cfg["empirical_normalization"]: - if device is not None: - self.obs_normalizer.to(device) - policy = lambda x: self.alg.actor_critic.act_inference(self.obs_normalizer(x)) # noqa: E731 - return policy - - def train_mode(self): - # -- PPO - self.alg.actor_critic.train() - # -- RND - if self.alg.rnd: - self.alg.rnd.train() - # -- Normalization - if self.empirical_normalization: - self.obs_normalizer.train() - self.critic_obs_normalizer.train() - - def eval_mode(self): - # -- PPO - self.alg.actor_critic.eval() - # -- RND - if self.alg.rnd: - self.alg.rnd.eval() - # -- Normalization - if self.empirical_normalization: - self.obs_normalizer.eval() - self.critic_obs_normalizer.eval() - - def add_git_repo_to_log(self, repo_file_path): - self.git_status_repos.append(repo_file_path) diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/runners/original_on_policy_runner.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/runners/original_on_policy_runner.py deleted file mode 100644 index 9092662..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/runners/original_on_policy_runner.py +++ /dev/null @@ -1,442 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import os -import statistics -import time -import torch -from collections import deque - -import rsl_rl -from rsl_rl.algorithms import PPO -from rsl_rl.env import VecEnv -from rsl_rl.modules import ActorCritic, ActorCriticRecurrent, EmpiricalNormalization -from rsl_rl.utils import store_code_state - - -class OnPolicyRunner: - """On-policy runner for training and evaluation.""" - - def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"): - self.cfg = train_cfg - self.alg_cfg = train_cfg["algorithm"] - self.policy_cfg = train_cfg["policy"] - self.device = device - self.env = env - - # resolve dimensions of observations - obs, extras = self.env.get_observations() - num_obs = obs.shape[1] - if "critic" in extras["observations"]: - num_critic_obs = extras["observations"]["critic"].shape[1] - else: - num_critic_obs = num_obs - actor_critic_class = eval(self.policy_cfg.pop("class_name")) # ActorCritic - actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class( - num_obs, num_critic_obs, self.env.num_actions, **self.policy_cfg - ).to(self.device) - - # resolve dimension of rnd gated state - if "rnd_cfg" in self.alg_cfg: - # check if rnd gated state is present - rnd_state = extras["observations"].get("rnd_state") - if rnd_state is None: - raise ValueError("Observations for they key 'rnd_state' not found in infos['observations'].") - # get dimension of rnd gated state - num_rnd_state = rnd_state.shape[1] - # add rnd gated state to config - self.alg_cfg["rnd_cfg"]["num_state"] = num_rnd_state - # scale down the rnd weight with timestep (similar to how rewards are scaled down in legged_gym envs) - self.alg_cfg["rnd_cfg"]["weight"] *= env.dt - - # if using symmetry then pass the environment config object - if "symmetry_cfg" in self.alg_cfg: - # this is used by the symmetry function for handling different observation terms - self.alg_cfg["symmetry_cfg"]["_env"] = env - - # init algorithm - alg_class = eval(self.alg_cfg.pop("class_name")) # PPO - self.alg: PPO = alg_class(actor_critic, device=self.device, **self.alg_cfg) - - # store training configuration - self.num_steps_per_env = self.cfg["num_steps_per_env"] - self.save_interval = self.cfg["save_interval"] - self.empirical_normalization = self.cfg["empirical_normalization"] - if self.empirical_normalization: - self.obs_normalizer = EmpiricalNormalization(shape=[num_obs], until=1.0e8).to(self.device) - self.critic_obs_normalizer = EmpiricalNormalization(shape=[num_critic_obs], until=1.0e8).to(self.device) - else: - self.obs_normalizer = torch.nn.Identity().to(self.device) # no normalization - self.critic_obs_normalizer = torch.nn.Identity().to(self.device) # no normalization - # init storage and model - self.alg.init_storage( - self.env.num_envs, - self.num_steps_per_env, - [num_obs], - [num_critic_obs], - [self.env.num_actions], - ) - - # Log - self.log_dir = log_dir - self.writer = None - self.tot_timesteps = 0 - self.tot_time = 0 - self.current_learning_iteration = 0 - self.git_status_repos = [rsl_rl.__file__] - - def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): - # initialize writer - if self.log_dir is not None and self.writer is None: - # Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard. - self.logger_type = self.cfg.get("logger", "tensorboard") - self.logger_type = self.logger_type.lower() - - if self.logger_type == "neptune": - from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter - - self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg) - self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg) - elif self.logger_type == "wandb": - from rsl_rl.utils.wandb_utils import WandbSummaryWriter - - self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg) - self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg) - elif self.logger_type == "tensorboard": - from torch.utils.tensorboard import SummaryWriter - - self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10) - else: - raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.") - - # randomize initial episode lengths (for exploration) - if init_at_random_ep_len: - self.env.episode_length_buf = torch.randint_like( - self.env.episode_length_buf, high=int(self.env.max_episode_length) - ) - - # start learning - obs, extras = self.env.get_observations() - critic_obs = extras["observations"].get("critic", obs) - obs, critic_obs = obs.to(self.device), critic_obs.to(self.device) - self.train_mode() # switch to train mode (for dropout for example) - - # Book keeping - ep_infos = [] - rewbuffer = deque(maxlen=100) - lenbuffer = deque(maxlen=100) - cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - # create buffers for logging extrinsic and intrinsic rewards - if self.alg.rnd: - erewbuffer = deque(maxlen=100) - irewbuffer = deque(maxlen=100) - cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device) - - start_iter = self.current_learning_iteration - tot_iter = start_iter + num_learning_iterations - for it in range(start_iter, tot_iter): - start = time.time() - # Rollout - with torch.inference_mode(): - for _ in range(self.num_steps_per_env): - # Sample actions from policy - actions = self.alg.act(obs, critic_obs) - # Step environment - obs, rewards, dones, infos = self.env.step(actions.to(self.env.device)) - - # Move to the agent device - obs, rewards, dones = obs.to(self.device), rewards.to(self.device), dones.to(self.device) - - # Normalize observations - obs = self.obs_normalizer(obs) - # Extract critic observations and normalize - if "critic" in infos["observations"]: - critic_obs = self.critic_obs_normalizer(infos["observations"]["critic"].to(self.device)) - else: - critic_obs = obs - - # Intrinsic rewards (extracted here only for logging)! - intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None - - # Process env step and store in buffer - self.alg.process_env_step(rewards, dones, infos) - - if self.log_dir is not None: - # Book keeping - if "episode" in infos: - ep_infos.append(infos["episode"]) - elif "log" in infos: - ep_infos.append(infos["log"]) - # Update rewards - if self.alg.rnd: - cur_ereward_sum += rewards - cur_ireward_sum += intrinsic_rewards # type: ignore - cur_reward_sum += rewards + intrinsic_rewards - else: - cur_reward_sum += rewards - # Update episode length - cur_episode_length += 1 - # Clear data for completed episodes - # -- common - new_ids = (dones > 0).nonzero(as_tuple=False) - rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist()) - lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist()) - cur_reward_sum[new_ids] = 0 - cur_episode_length[new_ids] = 0 - # -- intrinsic and extrinsic rewards - if self.alg.rnd: - erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist()) - irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist()) - cur_ereward_sum[new_ids] = 0 - cur_ireward_sum[new_ids] = 0 - - stop = time.time() - collection_time = stop - start - - # Learning step - start = stop - self.alg.compute_returns(critic_obs) - - # Update policy - # Note: we keep arguments here since locals() loads them - mean_value_loss, mean_surrogate_loss, mean_entropy, mean_rnd_loss, mean_symmetry_loss = self.alg.update() - stop = time.time() - learn_time = stop - start - self.current_learning_iteration = it - - # Logging info and save checkpoint - if self.log_dir is not None: - # Log information - self.log(locals()) - # Save model - if it % self.save_interval == 0: - self.save(os.path.join(self.log_dir, f"model_{it}.pt")) - - # Clear episode infos - ep_infos.clear() - - # Save code state - if it == start_iter: - # obtain all the diff files - git_file_paths = store_code_state(self.log_dir, self.git_status_repos) - # if possible store them to wandb - if self.logger_type in ["wandb", "neptune"] and git_file_paths: - for path in git_file_paths: - self.writer.save_file(path) - - # Save the final model after training - if self.log_dir is not None: - self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt")) - - def log(self, locs: dict, width: int = 80, pad: int = 35): - self.tot_timesteps += self.num_steps_per_env * self.env.num_envs - self.tot_time += locs["collection_time"] + locs["learn_time"] - iteration_time = locs["collection_time"] + locs["learn_time"] - - # -- Episode info - ep_string = "" - if locs["ep_infos"]: - for key in locs["ep_infos"][0]: - infotensor = torch.tensor([], device=self.device) - for ep_info in locs["ep_infos"]: - # handle scalar and zero dimensional tensor infos - if key not in ep_info: - continue - if not isinstance(ep_info[key], torch.Tensor): - ep_info[key] = torch.Tensor([ep_info[key]]) - if len(ep_info[key].shape) == 0: - ep_info[key] = ep_info[key].unsqueeze(0) - infotensor = torch.cat((infotensor, ep_info[key].to(self.device))) - value = torch.mean(infotensor) - # log to logger and terminal - if "/" in key: - self.writer.add_scalar(key, value, locs["it"]) - ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n""" - else: - self.writer.add_scalar("Episode/" + key, value, locs["it"]) - ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n""" - mean_std = self.alg.actor_critic.std.mean() - fps = int(self.num_steps_per_env * self.env.num_envs / (locs["collection_time"] + locs["learn_time"])) - - # -- Losses - self.writer.add_scalar("Loss/value_function", locs["mean_value_loss"], locs["it"]) - self.writer.add_scalar("Loss/surrogate", locs["mean_surrogate_loss"], locs["it"]) - self.writer.add_scalar("Loss/entropy", locs["mean_entropy"], locs["it"]) - self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"]) - if self.alg.rnd: - self.writer.add_scalar("Loss/rnd", locs["mean_rnd_loss"], locs["it"]) - if self.alg.symmetry: - self.writer.add_scalar("Loss/symmetry", locs["mean_symmetry_loss"], locs["it"]) - - # -- Policy - self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"]) - - # -- Performance - self.writer.add_scalar("Perf/total_fps", fps, locs["it"]) - self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"]) - self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"]) - - # -- Training - if len(locs["rewbuffer"]) > 0: - # separate logging for intrinsic and extrinsic rewards - if self.alg.rnd: - self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"]) - self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"]) - self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"]) - # everything else - self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"]) - self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"]) - if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging - self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time) - self.writer.add_scalar( - "Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time - ) - - str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m " - - if len(locs["rewbuffer"]) > 0: - log_string = ( - f"""{'#' * width}\n""" - f"""{str.center(width, ' ')}\n\n""" - f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[ - 'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n""" - f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n""" - f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n""" - ) - - # -- For symmetry - if self.alg.symmetry: - log_string += f"""{'Symmetry loss:':>{pad}} {locs['mean_symmetry_loss']:.4f}\n""" - - log_string += f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n""" - - # -- For RND - if self.alg.rnd: - log_string += ( - f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n""" - f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n""" - ) - - log_string += f"""{'Mean total reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n""" - log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n""" - # f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n""" - # f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""") - else: - log_string = ( - f"""{'#' * width}\n""" - f"""{str.center(width, ' ')}\n\n""" - f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[ - 'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n""" - f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n""" - f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n""" - ) - # -- For symmetry - if self.alg.symmetry: - log_string += f"""{'Symmetry loss:':>{pad}} {locs['mean_symmetry_loss']:.4f}\n""" - - log_string += f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n""" - - # f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n""" - # f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""") - - log_string += ep_string - log_string += ( - f"""{'-' * width}\n""" - f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n""" - f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n""" - f"""{'Total time:':>{pad}} {self.tot_time:.2f}s\n""" - f"""{'ETA:':>{pad}} {self.tot_time / (locs['it'] + 1) * ( - locs['num_learning_iterations'] - locs['it']):.1f}s\n""" - ) - print(log_string) - - def save(self, path: str, infos=None): - # -- Save PPO model - saved_dict = { - "model_state_dict": self.alg.actor_critic.state_dict(), - "optimizer_state_dict": self.alg.optimizer.state_dict(), - "iter": self.current_learning_iteration, - "infos": infos, - } - # -- Save RND model if used - if self.alg.rnd: - saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict() - saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict() - # -- Save observation normalizer if used - if self.empirical_normalization: - saved_dict["obs_norm_state_dict"] = self.obs_normalizer.state_dict() - saved_dict["critic_obs_norm_state_dict"] = self.critic_obs_normalizer.state_dict() - torch.save(saved_dict, path) - - # Upload model to external logging service - if self.logger_type in ["neptune", "wandb"]: - self.writer.save_model(path, self.current_learning_iteration) - - def load(self, path: str, load_optimizer: bool = True): - loaded_dict = torch.load(path, weights_only=False) - # -- Load PPO model - self.alg.actor_critic.load_state_dict(loaded_dict["model_state_dict"]) - # -- Load RND model if used - if self.alg.rnd: - self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"]) - # -- Load observation normalizer if used - if self.empirical_normalization: - self.obs_normalizer.load_state_dict(loaded_dict["obs_norm_state_dict"]) - self.critic_obs_normalizer.load_state_dict(loaded_dict["critic_obs_norm_state_dict"]) - # -- Load optimizer if used - if load_optimizer: - # -- PPO - self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"]) - # -- RND optimizer if used - if self.alg.rnd: - self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"]) - # -- Load current learning iteration - self.current_learning_iteration = loaded_dict["iter"] - return loaded_dict["infos"] - - def get_inference_policy(self, device=None): - self.eval_mode() # switch to evaluation mode (dropout for example) - if device is not None: - self.alg.actor_critic.to(device) - policy = self.alg.actor_critic.act_inference - if self.cfg["empirical_normalization"]: - if device is not None: - self.obs_normalizer.to(device) - policy = lambda x: self.alg.actor_critic.act_inference(self.obs_normalizer(x)) # noqa: E731 - return policy - - def train_mode(self): - # -- PPO - self.alg.actor_critic.train() - # -- RND - if self.alg.rnd: - self.alg.rnd.train() - # -- Normalization - if self.empirical_normalization: - self.obs_normalizer.train() - self.critic_obs_normalizer.train() - - def eval_mode(self): - # -- PPO - self.alg.actor_critic.eval() - # -- RND - if self.alg.rnd: - self.alg.rnd.eval() - # -- Normalization - if self.empirical_normalization: - self.obs_normalizer.eval() - self.critic_obs_normalizer.eval() - - def add_git_repo_to_log(self, repo_file_path): - self.git_status_repos.append(repo_file_path) diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/storage/replay_storage.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/storage/replay_storage.py deleted file mode 100644 index 4696959..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/storage/replay_storage.py +++ /dev/null @@ -1,357 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import torch - -from rsl_rl.utils import split_and_pad_trajectories - - -class ReplayStorage: - class Transition: - def __init__(self): - self.observations = None - self.critic_observations = None - self.actions = None - self.rewards = None - self.dones = None - - # For Policy Gradient-like algorithms - self.values = None - self.actions_log_prob = None - self.action_mean = None - self.action_sigma = None - - # For RNN-based policies - self.hidden_states = None - - # For RND - self.rnd_state = None - - # For BC (behavior cloning) - self.expert_action_mean = None - self.expert_action_sigma = None - - def clear(self): - self.__init__() - - def __init__( - self, - num_envs, - capacity: int, - obs_shape=None, - privileged_obs_shape=None, - actions_shape=None, - rnd_state_shape=None, - expert_actions_shape=None, - expert_actions_sigma_shape=None, - device="cpu", - ): - # store inputs - self.device = device - self.capacity = capacity - self.num_envs = num_envs - self.obs_shape = obs_shape - self.privileged_obs_shape = privileged_obs_shape - self.rnd_state_shape = rnd_state_shape - self.actions_shape = actions_shape - self.expert_action_mean_shape = expert_actions_shape - self.expert_action_sigma_shape = expert_actions_sigma_shape - - # Core - if obs_shape is not None: - self.observations = torch.zeros(capacity, num_envs, *obs_shape, device=device) - if privileged_obs_shape is not None: - self.privileged_observations = torch.zeros(capacity, num_envs, *privileged_obs_shape, device=device) - else: - self.privileged_observations = None - - if actions_shape is not None: - self.rewards = torch.zeros(capacity, num_envs, 1, device=self.device) - self.actions = torch.zeros(capacity, num_envs, *actions_shape, device=self.device) - self.dones = torch.zeros(capacity, num_envs, 1, device=self.device).byte() - - # For Policy-Gradient-like algorithms - self.actions_log_prob = torch.zeros(capacity, num_envs, 1, device=self.device) - self.values = torch.zeros(capacity, num_envs, 1, device=self.device) - self.returns = torch.zeros(capacity, num_envs, 1, device=self.device) - self.advantages = torch.zeros(capacity, num_envs, 1, device=self.device) - self.mu = torch.zeros(capacity, num_envs, *actions_shape, device=self.device) - self.sigma = torch.zeros(capacity, num_envs, *actions_shape, device=self.device) - - # For BC - if expert_actions_shape is not None: - self.expert_action_mean = torch.zeros(capacity, num_envs, *expert_actions_shape, device=self.device) - if expert_actions_sigma_shape is not None: - self.expert_action_sigma = torch.zeros(capacity, num_envs, *expert_actions_sigma_shape, device=self.device) - - # For RND - if rnd_state_shape is not None: - self.rnd_state = torch.zeros(capacity, num_envs, *rnd_state_shape, device=self.device) - - # For RNN networks - self.saved_hidden_states_a = None - self.saved_hidden_states_c = None - - # Circular buffer pointers - self.step = 0 - self.size = 0 - self.is_full = False - - def add_transitions(self, transition: Transition): - # check if the transition is valid - if self.size >= self.capacity: - self.is_full = True - # Core - if self.obs_shape is not None: - self.observations[self.step].copy_(transition.observations) - if self.privileged_observations is not None: - self.privileged_observations[self.step].copy_(transition.critic_observations) - if self.actions_shape is not None: - self.actions[self.step].copy_(transition.actions) - self.rewards[self.step].copy_(transition.rewards.view(-1, 1)) - self.dones[self.step].copy_(transition.dones.view(-1, 1)) - - # For Gradient-like algorithms - self.values[self.step].copy_(transition.values) - self.actions_log_prob[self.step].copy_(transition.actions_log_prob.view(-1, 1)) - self.mu[self.step].copy_(transition.action_mean) - self.sigma[self.step].copy_(transition.action_sigma) - - # For BC - if transition.expert_action_mean is not None and self.expert_action_mean is not None: - self.expert_action_mean[self.step].copy_(transition.expert_action_mean) - if transition.expert_action_sigma is not None and self.expert_action_sigma is not None: - self.expert_action_sigma[self.step].copy_(transition.expert_action_sigma) - - # For RND - if self.rnd_state_shape is not None: - self.rnd_state[self.step].copy_(transition.rnd_state) - - # For RNN networks - self._save_hidden_states(transition.hidden_states) - - # Update circular buffer pointers - self.step = (self.step + 1) % self.capacity - self.size = min(self.size + 1, self.capacity) - - def _save_hidden_states(self, hidden_states): - if hidden_states is None or hidden_states == (None, None): - return - # make a tuple out of GRU hidden state sto match the LSTM format - hid_a = hidden_states[0] if isinstance(hidden_states[0], tuple) else (hidden_states[0],) - hid_c = hidden_states[1] if isinstance(hidden_states[1], tuple) else (hidden_states[1],) - - # initialize if needed - if self.saved_hidden_states_a is None: - self.saved_hidden_states_a = [ - torch.zeros(self.observations.shape[0], *hid_a[i].shape, device=self.device) for i in range(len(hid_a)) - ] - self.saved_hidden_states_c = [ - torch.zeros(self.observations.shape[0], *hid_c[i].shape, device=self.device) for i in range(len(hid_c)) - ] - # copy the states - for i in range(len(hid_a)): - self.saved_hidden_states_a[i][self.step].copy_(hid_a[i]) - self.saved_hidden_states_c[i][self.step].copy_(hid_c[i]) - - def clear(self): - self.step = 0 - self.size = 0 - self.is_full = False - - def compute_returns(self, last_values, gamma, lam): - advantage = 0 - for step in reversed(range(self.size)): - # if we are at the last step, bootstrap the return value - if step == self.size - 1: - next_values = last_values - else: - next_values = self.values[step + 1] - # 1 if we are not in a terminal state, 0 otherwise - next_is_not_terminal = 1.0 - self.dones[step].float() - # TD error: r_t + gamma * V(s_{t+1}) - V(s_t) - delta = self.rewards[step] + next_is_not_terminal * gamma * next_values - self.values[step] - # Advantage: A(s_t, a_t) = delta_t + gamma * lambda * A(s_{t+1}, a_{t+1}) - advantage = delta + next_is_not_terminal * gamma * lam * advantage - # Return: R_t = A(s_t, a_t) + V(s_t) - self.returns[step] = advantage + self.values[step] - - # Compute and normalize the advantages - self.advantages = self.returns[: self.size] - self.values[: self.size] - self.advantages[: self.size] = (self.advantages - self.advantages.mean()) / (self.advantages.std() + 1e-8) - - def get_statistics(self): - done = self.dones[: self.size] - done[-1] = 1 - flat_dones = done.permute(1, 0, 2).reshape(-1, 1) - done_indices = torch.cat( - (flat_dones.new_tensor([-1], dtype=torch.int64), flat_dones.nonzero(as_tuple=False)[:, 0]) - ) - trajectory_lengths = done_indices[1:] - done_indices[:-1] - return trajectory_lengths.float().mean(), self.rewards[: self.size].mean() - - def mini_batch_generator(self, num_mini_batches, num_epochs=8): - batch_size = self.num_envs * self.size - mini_batch_size = batch_size // num_mini_batches - indices = torch.randperm(num_mini_batches * mini_batch_size, requires_grad=False, device=self.device) - - # Core - if self.observations is not None: - observations = self.observations[: self.size].flatten(0, 1) - if self.privileged_observations is not None: - critic_observations = self.privileged_observations[: self.size].flatten(0, 1) - else: - critic_observations = observations - - if self.actions is not None: - actions = self.actions[: self.size].flatten(0, 1) - values = self.values[: self.size].flatten(0, 1) - returns = self.returns[: self.size].flatten(0, 1) - - # For PPO - old_actions_log_prob = self.actions_log_prob[: self.size].flatten(0, 1) - advantages = self.advantages[: self.size].flatten(0, 1) - old_mu = self.mu[: self.size].flatten(0, 1) - old_sigma = self.sigma[: self.size].flatten(0, 1) - # For BC - if self.expert_action_mean_shape is not None: - expert_action_mu = self.expert_action_mean[: self.size].flatten(0, 1) - if self.expert_action_sigma_shape is not None: - expert_action_sigma = self.expert_action_sigma[: self.size].flatten(0, 1) - - # For RND - if self.rnd_state_shape is not None: - rnd_state = self.rnd_state[: self.size].flatten(0, 1) - - for epoch in range(num_epochs): - for i in range(num_mini_batches): - # Select the indices for the mini-batch - start = i * mini_batch_size - end = (i + 1) * mini_batch_size - batch_idx = indices[start:end] - - # Create the mini-batch - # -- Core - obs_batch, critic_observations_batch, actions_batch = None, None, None - if self.observations is not None: - obs_batch = observations[batch_idx] - critic_observations_batch = critic_observations[batch_idx] - if self.actions is not None: - actions_batch = actions[batch_idx] - - # -- For PPO - target_values_batch, returns_batch, advantages_batch = None, None, None - old_actions_log_prob_batch, old_mu_batch, old_sigma_batch = None, None, None - if self.actions is not None: - target_values_batch = values[batch_idx] - returns_batch = returns[batch_idx] - old_actions_log_prob_batch = old_actions_log_prob[batch_idx] - advantages_batch = advantages[batch_idx] - old_mu_batch = old_mu[batch_idx] - old_sigma_batch = old_sigma[batch_idx] - # -- For BC - expert_action_mu_batch, expert_action_sigma_batch = None, None - if self.expert_action_mean_shape is not None: - expert_action_mu_batch = expert_action_mu[batch_idx] - if self.expert_action_sigma_shape is not None: - expert_action_sigma_batch = expert_action_sigma[batch_idx] - - # -- For RND - rnd_state_batch = None - if self.rnd_state_shape is not None: - rnd_state_batch = rnd_state[batch_idx] - - # Yield the mini-batch - yield obs_batch, critic_observations_batch, actions_batch, target_values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, ( - None, - None, - ), None, expert_action_mu_batch, expert_action_sigma_batch, rnd_state_batch - - # for RNNs only - def recurrent_mini_batch_generator(self, num_mini_batches, num_epochs=8): - padded_obs_trajectories, trajectory_masks = split_and_pad_trajectories(self.observations, self.dones) - if self.privileged_observations is not None: - padded_critic_obs_trajectories, _ = split_and_pad_trajectories(self.privileged_observations, self.dones) - else: - padded_critic_obs_trajectories = padded_obs_trajectories - - if self.rnd_state_shape is not None: - padded_rnd_state_trajectories, _ = split_and_pad_trajectories(self.rnd_state, self.dones) - else: - padded_rnd_state_trajectories = None - - mini_batch_size = self.num_envs // num_mini_batches - for ep in range(num_epochs): - first_traj = 0 - for i in range(num_mini_batches): - start = i * mini_batch_size - stop = (i + 1) * mini_batch_size - - dones = self.dones.squeeze(-1) - last_was_done = torch.zeros_like(dones, dtype=torch.bool) - last_was_done[1:] = dones[:-1] - last_was_done[0] = True - trajectories_batch_size = torch.sum(last_was_done[:, start:stop]) - last_traj = first_traj + trajectories_batch_size - - masks_batch = trajectory_masks[:, first_traj:last_traj] - obs_batch = padded_obs_trajectories[:, first_traj:last_traj] - critic_obs_batch = padded_critic_obs_trajectories[:, first_traj:last_traj] - - # For BC - if self.expert_action_mean_shape is not None: - expert_action_mu_batch = self.expert_action_mean[:, start:stop] - else: - expert_action_mu_batch = None - if self.expert_action_sigma_shape is not None: - expert_action_sigma_batch = self.expert_action_sigma[:, start:stop] - else: - expert_action_sigma_batch = None - - if padded_rnd_state_trajectories is not None: - rnd_state_batch = padded_rnd_state_trajectories[:, first_traj:last_traj] - else: - rnd_state_batch = None - actions_batch = self.actions[:, start:stop] - old_mu_batch = self.mu[:, start:stop] - old_sigma_batch = self.sigma[:, start:stop] - returns_batch = self.returns[:, start:stop] - advantages_batch = self.advantages[:, start:stop] - values_batch = self.values[:, start:stop] - old_actions_log_prob_batch = self.actions_log_prob[:, start:stop] - - # reshape to [num_envs, time, num layers, hidden dim] (original shape: [time, num_layers, num_envs, hidden_dim]) - # then take only time steps after dones (flattens num envs and time dimensions), - # take a batch of trajectories and finally reshape back to [num_layers, batch, hidden_dim] - last_was_done = last_was_done.permute(1, 0) - hid_a_batch = [ - saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj] - .transpose(1, 0) - .contiguous() - for saved_hidden_states in self.saved_hidden_states_a - ] - hid_c_batch = [ - saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj] - .transpose(1, 0) - .contiguous() - for saved_hidden_states in self.saved_hidden_states_c - ] - # remove the tuple for GRU - hid_a_batch = hid_a_batch[0] if len(hid_a_batch) == 1 else hid_a_batch - hid_c_batch = hid_c_batch[0] if len(hid_c_batch) == 1 else hid_c_batch - - yield obs_batch, critic_obs_batch, actions_batch, values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, ( - hid_a_batch, - hid_c_batch, - ), masks_batch, expert_action_mu_batch, expert_action_sigma_batch, rnd_state_batch - - first_traj = last_traj diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/storage/rollout_storage.py b/source/uwlab_rl/uwlab_rl/rsl_rl/ext/storage/rollout_storage.py deleted file mode 100644 index eca2846..0000000 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/ext/storage/rollout_storage.py +++ /dev/null @@ -1,344 +0,0 @@ -# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import torch - -from rsl_rl.utils import split_and_pad_trajectories - - -class RolloutStorage: - class Transition: - def __init__(self): - self.observations = None - self.critic_observations = None - self.actions = None - self.rewards = None - self.dones = None - self.values = None - self.actions_log_prob = None - self.action_mean = None - self.action_sigma = None - self.hidden_states = None - self.rnd_state = None - self.expert_action_mean = None - self.expert_action_sigma = None - - def clear(self): - self.__init__() - - def __init__( - self, - num_envs, - num_transitions_per_env, - obs_shape, - privileged_obs_shape, - actions_shape, - rnd_state_shape=None, - expert_action_mean_shape=None, - expert_action_sigma_shape=None, - device="cpu", - ): - # store inputs - self.device = device - self.num_transitions_per_env = num_transitions_per_env - self.num_envs = num_envs - self.obs_shape = obs_shape - self.privileged_obs_shape = privileged_obs_shape - self.rnd_state_shape = rnd_state_shape - self.actions_shape = actions_shape - - # For bc - self.expert_action_mean_shape = expert_action_mean_shape - self.expert_action_sigma_shape = expert_action_sigma_shape - - # Core - self.observations = torch.zeros(num_transitions_per_env, num_envs, *obs_shape, device=self.device) - if privileged_obs_shape is not None: - self.privileged_observations = torch.zeros( - num_transitions_per_env, num_envs, *privileged_obs_shape, device=self.device - ) - else: - self.privileged_observations = None - self.rewards = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device) - self.actions = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device) - self.dones = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device).byte() - - # For PPO - self.actions_log_prob = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device) - self.values = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device) - self.returns = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device) - self.advantages = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device) - self.mu = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device) - self.sigma = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device) - - # For BC - if expert_action_mean_shape is not None: - self.expert_action_mean = torch.zeros( - num_transitions_per_env, num_envs, *expert_action_mean_shape, device=self.device - ) - if expert_action_sigma_shape is not None: - self.expert_action_sigma = torch.zeros( - num_transitions_per_env, num_envs, *expert_action_sigma_shape, device=self.device - ) - - # For RND - if rnd_state_shape is not None: - self.rnd_state = torch.zeros(num_transitions_per_env, num_envs, *rnd_state_shape, device=self.device) - - # For RNN networks - self.saved_hidden_states_a = None - self.saved_hidden_states_c = None - # counter for the number of transitions stored - self.step = 0 - - def add_transitions(self, transition: Transition): - # check if the transition is valid - if self.step >= self.num_transitions_per_env: - raise OverflowError("Rollout buffer overflow! You should call clear() before adding new transitions.") - - # Core - self.observations[self.step].copy_(transition.observations) - if self.privileged_observations is not None: - self.privileged_observations[self.step].copy_(transition.critic_observations) - self.actions[self.step].copy_(transition.actions) - self.rewards[self.step].copy_(transition.rewards.view(-1, 1)) - self.dones[self.step].copy_(transition.dones.view(-1, 1)) - - # For PPO - self.values[self.step].copy_(transition.values) - self.actions_log_prob[self.step].copy_(transition.actions_log_prob.view(-1, 1)) - self.mu[self.step].copy_(transition.action_mean) - self.sigma[self.step].copy_(transition.action_sigma) - - # For BC - if transition.expert_action_mean is not None: - self.expert_action_mean[self.step].copy_(transition.expert_action_mean) - if transition.expert_action_sigma is not None: - self.expert_action_sigma[self.step].copy_(transition.expert_action_sigma) - - # For RND - if self.rnd_state_shape is not None: - self.rnd_state[self.step].copy_(transition.rnd_state) - - # For RNN networks - self._save_hidden_states(transition.hidden_states) - - # increment the counter - self.step += 1 - - def _save_hidden_states(self, hidden_states): - if hidden_states is None or hidden_states == (None, None): - return - # make a tuple out of GRU hidden state sto match the LSTM format - hid_a = hidden_states[0] if isinstance(hidden_states[0], tuple) else (hidden_states[0],) - hid_c = hidden_states[1] if isinstance(hidden_states[1], tuple) else (hidden_states[1],) - - # initialize if needed - if self.saved_hidden_states_a is None: - self.saved_hidden_states_a = [ - torch.zeros(self.observations.shape[0], *hid_a[i].shape, device=self.device) for i in range(len(hid_a)) - ] - self.saved_hidden_states_c = [ - torch.zeros(self.observations.shape[0], *hid_c[i].shape, device=self.device) for i in range(len(hid_c)) - ] - # copy the states - for i in range(len(hid_a)): - self.saved_hidden_states_a[i][self.step].copy_(hid_a[i]) - self.saved_hidden_states_c[i][self.step].copy_(hid_c[i]) - - def clear(self): - self.step = 0 - - def compute_returns(self, last_values, gamma, lam): - advantage = 0 - for step in reversed(range(self.num_transitions_per_env)): - # if we are at the last step, bootstrap the return value - if step == self.num_transitions_per_env - 1: - next_values = last_values - else: - next_values = self.values[step + 1] - # 1 if we are not in a terminal state, 0 otherwise - next_is_not_terminal = 1.0 - self.dones[step].float() - # TD error: r_t + gamma * V(s_{t+1}) - V(s_t) - delta = self.rewards[step] + next_is_not_terminal * gamma * next_values - self.values[step] - # Advantage: A(s_t, a_t) = delta_t + gamma * lambda * A(s_{t+1}, a_{t+1}) - advantage = delta + next_is_not_terminal * gamma * lam * advantage - # Return: R_t = A(s_t, a_t) + V(s_t) - self.returns[step] = advantage + self.values[step] - - # Compute and normalize the advantages - self.advantages = self.returns - self.values - self.advantages = (self.advantages - self.advantages.mean()) / (self.advantages.std() + 1e-8) - - def get_statistics(self): - done = self.dones - done[-1] = 1 - flat_dones = done.permute(1, 0, 2).reshape(-1, 1) - done_indices = torch.cat( - (flat_dones.new_tensor([-1], dtype=torch.int64), flat_dones.nonzero(as_tuple=False)[:, 0]) - ) - trajectory_lengths = done_indices[1:] - done_indices[:-1] - return trajectory_lengths.float().mean(), self.rewards.mean() - - def mini_batch_generator(self, num_mini_batches, num_epochs=8): - batch_size = self.num_envs * self.num_transitions_per_env - mini_batch_size = batch_size // num_mini_batches - indices = torch.randperm(num_mini_batches * mini_batch_size, requires_grad=False, device=self.device) - - # Core - observations = self.observations.flatten(0, 1) - if self.privileged_observations is not None: - critic_observations = self.privileged_observations.flatten(0, 1) - else: - critic_observations = observations - - actions = self.actions.flatten(0, 1) - values = self.values.flatten(0, 1) - returns = self.returns.flatten(0, 1) - - # For PPO - old_actions_log_prob = self.actions_log_prob.flatten(0, 1) - advantages = self.advantages.flatten(0, 1) - old_mu = self.mu.flatten(0, 1) - old_sigma = self.sigma.flatten(0, 1) - # For BC - if self.expert_action_mean_shape is not None: - expert_action_mu = self.expert_action_mean.flatten(0, 1) - if self.expert_action_sigma_shape is not None: - expert_action_sigma = self.expert_action_sigma.flatten(0, 1) - - # For RND - if self.rnd_state_shape is not None: - rnd_state = self.rnd_state.flatten(0, 1) - - for epoch in range(num_epochs): - for i in range(num_mini_batches): - # Select the indices for the mini-batch - start = i * mini_batch_size - end = (i + 1) * mini_batch_size - batch_idx = indices[start:end] - - # Create the mini-batch - # -- Core - obs_batch = observations[batch_idx] - critic_observations_batch = critic_observations[batch_idx] - actions_batch = actions[batch_idx] - - # -- For PPO - target_values_batch = values[batch_idx] - returns_batch = returns[batch_idx] - old_actions_log_prob_batch = old_actions_log_prob[batch_idx] - advantages_batch = advantages[batch_idx] - old_mu_batch = old_mu[batch_idx] - old_sigma_batch = old_sigma[batch_idx] - # -- For BC - if self.expert_action_mean_shape is not None: - expert_action_mu_batch = expert_action_mu[batch_idx] - else: - expert_action_mu_batch = None - if self.expert_action_sigma_shape is not None: - expert_action_sigma_batch = expert_action_sigma[batch_idx] - else: - expert_action_sigma_batch = None - - # -- For RND - if self.rnd_state_shape is not None: - rnd_state_batch = rnd_state[batch_idx] - else: - rnd_state_batch = None - - # Yield the mini-batch - yield obs_batch, critic_observations_batch, actions_batch, target_values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, ( - None, - None, - ), None, expert_action_mu_batch, expert_action_sigma_batch, rnd_state_batch - - # for RNNs only - def recurrent_mini_batch_generator(self, num_mini_batches, num_epochs=8): - padded_obs_trajectories, trajectory_masks = split_and_pad_trajectories(self.observations, self.dones) - if self.privileged_observations is not None: - padded_critic_obs_trajectories, _ = split_and_pad_trajectories(self.privileged_observations, self.dones) - else: - padded_critic_obs_trajectories = padded_obs_trajectories - - if self.rnd_state_shape is not None: - padded_rnd_state_trajectories, _ = split_and_pad_trajectories(self.rnd_state, self.dones) - else: - padded_rnd_state_trajectories = None - - mini_batch_size = self.num_envs // num_mini_batches - for ep in range(num_epochs): - first_traj = 0 - for i in range(num_mini_batches): - start = i * mini_batch_size - stop = (i + 1) * mini_batch_size - - dones = self.dones.squeeze(-1) - last_was_done = torch.zeros_like(dones, dtype=torch.bool) - last_was_done[1:] = dones[:-1] - last_was_done[0] = True - trajectories_batch_size = torch.sum(last_was_done[:, start:stop]) - last_traj = first_traj + trajectories_batch_size - - masks_batch = trajectory_masks[:, first_traj:last_traj] - obs_batch = padded_obs_trajectories[:, first_traj:last_traj] - critic_obs_batch = padded_critic_obs_trajectories[:, first_traj:last_traj] - - # For BC - if self.expert_action_mean_shape is not None: - expert_action_mu_batch = self.expert_action_mean[:, start:stop] - else: - expert_action_mu_batch = None - if self.expert_action_sigma_shape is not None: - expert_action_sigma_batch = self.expert_action_sigma[:, start:stop] - else: - expert_action_sigma_batch = None - - if padded_rnd_state_trajectories is not None: - rnd_state_batch = padded_rnd_state_trajectories[:, first_traj:last_traj] - else: - rnd_state_batch = None - - actions_batch = self.actions[:, start:stop] - old_mu_batch = self.mu[:, start:stop] - old_sigma_batch = self.sigma[:, start:stop] - returns_batch = self.returns[:, start:stop] - advantages_batch = self.advantages[:, start:stop] - values_batch = self.values[:, start:stop] - old_actions_log_prob_batch = self.actions_log_prob[:, start:stop] - - # reshape to [num_envs, time, num layers, hidden dim] (original shape: [time, num_layers, num_envs, hidden_dim]) - # then take only time steps after dones (flattens num envs and time dimensions), - # take a batch of trajectories and finally reshape back to [num_layers, batch, hidden_dim] - last_was_done = last_was_done.permute(1, 0) - hid_a_batch = [ - saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj] - .transpose(1, 0) - .contiguous() - for saved_hidden_states in self.saved_hidden_states_a - ] - hid_c_batch = [ - saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj] - .transpose(1, 0) - .contiguous() - for saved_hidden_states in self.saved_hidden_states_c - ] - # remove the tuple for GRU - hid_a_batch = hid_a_batch[0] if len(hid_a_batch) == 1 else hid_a_batch - hid_c_batch = hid_c_batch[0] if len(hid_c_batch) == 1 else hid_c_batch - - yield obs_batch, critic_obs_batch, actions_batch, values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, ( - hid_a_batch, - hid_c_batch, - ), masks_batch, expert_action_mu_batch, expert_action_sigma_batch, rnd_state_batch - - first_traj = last_traj diff --git a/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py b/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py index 018cd20..8d3eb98 100644 --- a/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py +++ b/source/uwlab_rl/uwlab_rl/rsl_rl/rl_cfg.py @@ -1,22 +1,13 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from dataclasses import MISSING +from typing import Literal from isaaclab.utils import configclass - -from isaaclab_rl.rsl_rl import RslRlPpoAlgorithmCfg # noqa: F401 - - -@configclass -class SymmetryCfg: - use_data_augmentation: bool = False - - use_mirror_loss: bool = False - - data_augmentation_func: callable = None +from isaaclab_rl.rsl_rl import RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg # noqa: F401 @configclass @@ -63,13 +54,21 @@ class OffPolicyAlgorithmCfg: """The configuration for the offline behavior cloning(dagger).""" +@configclass +class RslRlFancyActorCriticCfg(RslRlPpoActorCriticCfg): + """Configuration for the fancy actor-critic networks.""" + + state_dependent_std: bool = False + """Whether to use state-dependent standard deviation.""" + + noise_std_type: Literal["scalar", "log", "gsde"] = "scalar" + """The type of noise standard deviation for the policy. Default is scalar.""" + + @configclass class RslRlFancyPpoAlgorithmCfg(RslRlPpoAlgorithmCfg): """Configuration for the PPO algorithm.""" - symmetry_cfg: SymmetryCfg | None = None - """The configuration for the symmetry.""" - behavior_cloning_cfg: BehaviorCloningCfg | None = None """The configuration for the online behavior cloning.""" diff --git a/source/uwlab_rl/uwlab_rl/skrl/__init__.py b/source/uwlab_rl/uwlab_rl/skrl/__init__.py index 937c60e..14d970b 100644 --- a/source/uwlab_rl/uwlab_rl/skrl/__init__.py +++ b/source/uwlab_rl/uwlab_rl/skrl/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_rl/uwlab_rl/skrl/extensions/__init__.py b/source/uwlab_rl/uwlab_rl/skrl/extensions/__init__.py index b2e1041..abf7a10 100644 --- a/source/uwlab_rl/uwlab_rl/skrl/extensions/__init__.py +++ b/source/uwlab_rl/uwlab_rl/skrl/extensions/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_rl/uwlab_rl/skrl/extensions/ext_cfg.py b/source/uwlab_rl/uwlab_rl/skrl/extensions/ext_cfg.py index 3e9a939..64dd7df 100644 --- a/source/uwlab_rl/uwlab_rl/skrl/extensions/ext_cfg.py +++ b/source/uwlab_rl/uwlab_rl/skrl/extensions/ext_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,8 +6,9 @@ from __future__ import annotations import torch +from collections.abc import Callable from dataclasses import MISSING -from typing import Any, Callable +from typing import Any from isaaclab.utils import configclass diff --git a/source/uwlab_rl/uwlab_rl/skrl/extensions/loss_ext.py b/source/uwlab_rl/uwlab_rl/skrl/extensions/loss_ext.py index f8e14b8..69550ff 100644 --- a/source/uwlab_rl/uwlab_rl/skrl/extensions/loss_ext.py +++ b/source/uwlab_rl/uwlab_rl/skrl/extensions/loss_ext.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,12 +6,12 @@ from __future__ import annotations import torch -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv from skrl.agents.torch.base import Agent - - from uwlab.envs import DataManagerBasedRLEnv """ loss extension functions return a loss value given a batch of samples. they can be used to add additional loss terms to the agent's loss function. @@ -19,7 +19,7 @@ def expert_distillation_loss_f( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, agent: Agent, context: dict[str, Any], criterion: torch.nn.Module, diff --git a/source/uwlab_rl/uwlab_rl/skrl/extensions/patches.py b/source/uwlab_rl/uwlab_rl/skrl/extensions/patches.py index a6e60c9..7169eb6 100644 --- a/source/uwlab_rl/uwlab_rl/skrl/extensions/patches.py +++ b/source/uwlab_rl/uwlab_rl/skrl/extensions/patches.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,18 +6,17 @@ import torch import types from contextlib import contextmanager -from typing import Any, Dict, List +from typing import Any +from isaaclab.envs import ManagerBasedRLEnv from skrl.agents.torch.base import Agent -from uwlab.envs import DataManagerBasedRLEnv - from .ext_cfg import ContextInitializerCfg, SupplementaryTrainingCfg class AgentPatcher: def __init__( - self, locs: Dict[str, Any], env: DataManagerBasedRLEnv, agent: Agent, suppl_train_cfg: SupplementaryTrainingCfg + self, locs: dict[str, Any], env: ManagerBasedRLEnv, agent: Agent, suppl_train_cfg: SupplementaryTrainingCfg ): self.env = env self.agent = agent @@ -30,10 +29,10 @@ def __init__( def context_init( self, - locs: Dict[str, Any], - env: DataManagerBasedRLEnv, + locs: dict[str, Any], + env: ManagerBasedRLEnv, agent: Agent, - context_initializers: List[ContextInitializerCfg], + context_initializers: list[ContextInitializerCfg], ): context = {} for context_initializer in context_initializers: diff --git a/source/uwlab_rl/uwlab_rl/skrl/extensions/sample_ext.py b/source/uwlab_rl/uwlab_rl/skrl/extensions/sample_ext.py index 1fa1414..165b55c 100644 --- a/source/uwlab_rl/uwlab_rl/skrl/extensions/sample_ext.py +++ b/source/uwlab_rl/uwlab_rl/skrl/extensions/sample_ext.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,13 +6,13 @@ from __future__ import annotations import torch -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv from skrl.agents.torch.base import Agent - from uwlab.envs import DataManagerBasedRLEnv - """ Sample extension returns a function that takes a batch of samples and return the additional samples wish to be added to the memory. @@ -20,7 +20,7 @@ def experts_act_f( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, agent: Agent, context: dict, map_encoding_to_expert_key: str, @@ -30,7 +30,7 @@ def experts_act_f( Parameters: ---------- - env : DataManagerBasedRLEnv + env : ManagerBasedRLEnv The IsaacLab Manager-based RL environment agent : Agent diff --git a/source/uwlab_rl/uwlab_rl/skrl/ppo_cfg.py b/source/uwlab_rl/uwlab_rl/skrl/ppo_cfg.py index 6054f09..c4cbaee 100644 --- a/source/uwlab_rl/uwlab_rl/skrl/ppo_cfg.py +++ b/source/uwlab_rl/uwlab_rl/skrl/ppo_cfg.py @@ -1,11 +1,11 @@ # base_config.py -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from dataclasses import MISSING, field -from typing import Dict, List, Literal, Optional, Union +from typing import Literal from isaaclab.utils import configclass @@ -23,8 +23,8 @@ class ModelPolicyCfg: min_log_std: float = MISSING max_log_std: float = MISSING input_shape: str = MISSING - hiddens: List[int] = MISSING - hidden_activation: List[str] = MISSING + hiddens: list[int] = MISSING + hidden_activation: list[str] = MISSING output_shape: str = MISSING output_activation: str = MISSING output_scale: float = MISSING @@ -37,8 +37,8 @@ class ModelValueCfg: value_class: str = MISSING clip_actions: bool = MISSING input_shape: str = MISSING - hiddens: List[int] = MISSING - hidden_activation: List[str] = MISSING + hiddens: list[int] = MISSING + hidden_activation: list[str] = MISSING output_shape: str = MISSING output_activation: str = MISSING output_scale: float = MISSING @@ -62,7 +62,7 @@ class ExperimentCfg: write_interval: int = MISSING checkpoint_interval: int = MISSING wandb: bool = MISSING - wandb_kwargs: Dict[str, str] = field(default_factory=dict) + wandb_kwargs: dict[str, str] = field(default_factory=dict) @configclass @@ -77,11 +77,11 @@ class PPOAgentCfg: lambda_: float = MISSING learning_rate: float = MISSING learning_rate_scheduler: str = MISSING - learning_rate_scheduler_kwargs: Dict[str, Union[float, str]] = field(default_factory=dict) + learning_rate_scheduler_kwargs: dict[str, float | str] = field(default_factory=dict) state_preprocessor: str = MISSING - state_preprocessor_kwargs: Optional[Dict] = None + state_preprocessor_kwargs: dict | None = None value_preprocessor: str = MISSING - value_preprocessor_kwargs: Optional[Dict] = None + value_preprocessor_kwargs: dict | None = None random_timesteps: int = MISSING learning_starts: int = MISSING grad_norm_clip: float = MISSING @@ -117,7 +117,7 @@ class SKRLConfig: models: ModelsCfg = MISSING agent: PPOAgentCfg = MISSING trainer: TrainerCfg = MISSING - extension: Optional[ExtensionCfg] = None + extension: ExtensionCfg | None = None def to_skrl_dict(self): dict = self.to_dict() diff --git a/source/uwlab_tasks/config/extension.toml b/source/uwlab_tasks/config/extension.toml index cf51722..37dbd4f 100644 --- a/source/uwlab_tasks/config/extension.toml +++ b/source/uwlab_tasks/config/extension.toml @@ -1,13 +1,13 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "0.13.4" +version = "0.13.8" # Description title = "UW Lab Tasks" readme = "docs/README.md" description="Extension for Isaac Lab" -repository = "https://github.com/UW-Lab/UWLab" +repository = "https://github.com/uw-lab/UWLab" category = "robotics" keywords = ["robotics", "rl", "il", "learning"] diff --git a/source/uwlab_tasks/docs/CHANGELOG.rst b/source/uwlab_tasks/docs/CHANGELOG.rst index fe8c91f..4405010 100644 --- a/source/uwlab_tasks/docs/CHANGELOG.rst +++ b/source/uwlab_tasks/docs/CHANGELOG.rst @@ -1,7 +1,46 @@ Changelog --------- +0.13.8 (2025-10-24) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Moved hydra utilities from uwlab apps to tasks + + + +0.13.7 (2025-10-09) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Removed Leap6D robot and it related track environment as it is not used any more + + + +0.13.6 (2025-10-09) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed the terrain position patching style to be minimal + + +0.13.5 (2025-09-27) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Fixed spot augmentation function not working with the most up to date rsl-rl-lib==3.0.1 + + 0.13.4 (2025-03-23) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixed ^^^^^ @@ -10,15 +49,17 @@ Fixed 0.13.3 (2025-03-23) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixed ^^^^^ * fixed the contains relationship does not work for x in env.scene because it doesn't have __contains__ -property, the solution is to use x in env.scene.keys() with # noqa: SIM118 to suppress flake8 complaint + property, the solution is to use x in env.scene.keys() with # noqa: SIM118 to suppress flake8 complaint 0.13.2 (2025-03-23) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixed ^^^^^ @@ -26,6 +67,7 @@ Fixed * the named mdp in accordance made in uwlab.envs.mdp for clarity 0.13.1 (2025-01-13) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixed ^^^^^ @@ -35,7 +77,7 @@ Fixed 0.13.0 (2024-11-10) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -44,7 +86,7 @@ Added 0.12.1 (2024-10-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -52,7 +94,7 @@ Added * added skrl ppo config for single cake decoration environment 0.12.0 (2024-10-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -61,7 +103,7 @@ Added * tested to be compatible with current evolution branch 0.11.0 (2024-10-20) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -69,7 +111,7 @@ Added * merged franka workshop environment, frank multi cake environment into uwlab, Thanks Yufeng! 0.10.0 (2024-10-20) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -77,7 +119,7 @@ Added * merged skrl workflow pipeline into uwlab 0.9.6 (2024-09-06) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -86,7 +128,7 @@ Changed changes at :func:`uwlab_tasks.tasks.locomotion.fetching.config.a1.__init__.py` 0.9.5 (2024-09-02) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -98,7 +140,7 @@ Changed 0.9.4 (2024-08-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -107,14 +149,14 @@ Changed 0.9.3 (2024-08-24) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ * separated out lift objects environment from lift hammer environment at tasks.manipulation.lift_objects 0.9.2 (2024-08-19) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -123,14 +165,14 @@ Changed and :class:`uwlab_tasks.tasks.manipulation.cake_decoration.config.hebi.tycho_joint_pos.IkabsoluteAction` 0.9.1 (2024-08-06) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^^^ -* Added necessary mdps for :folder:`uwlab_tasks.tasks.locomotion` tasks +* Added necessary mdps for :mod:`uwlab_tasks.tasks.locomotion` tasks 0.9.0 (2024-08-06) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -139,7 +181,7 @@ Changed 0.8.3 (2024-08-06) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -153,7 +195,7 @@ Changed 0.8.2 (2024-08-06) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -162,7 +204,7 @@ Added 0.8.1 (2024-08-06) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixed ^^^^^ @@ -171,7 +213,7 @@ Fixed 0.8.0 (2024-07-29) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fixed ^^^^^ @@ -180,7 +222,7 @@ Fixed 0.8.0 (2024-07-29) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -189,16 +231,16 @@ Added 0.7.0 (2024-07-29) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ * added Unitree Go1 Go2 and spot for Fetching task at - :folder:`uwlab_tasks.tasks.locomotion.fetching` + :mod:`uwlab_tasks.tasks.locomotion.fetching` 0.6.1 (2024-07-29) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -207,7 +249,7 @@ Changed 0.6.0 (2024-07-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -216,7 +258,7 @@ Changed 0.5.2 (2024-07-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -225,16 +267,16 @@ Changed 'UW-LiftObjects-XarmLeap-IkDel-v0' 0.5.1 (2024-07-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ * support IkDelta action for environment LiftObjectsXarmLeap at - :folder:`uwlab_tasks.tasks.manipulation.lift_objects` + :mod:`uwlab_tasks.tasks.manipulation.lift_objects` 0.5.0 (2024-07-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -242,7 +284,7 @@ Changed 0.4.3 (2024-07-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -250,7 +292,7 @@ Changed 0.4.2 (2024-07-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -259,61 +301,61 @@ Changed 0.4.1 (2024-07-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ -* update track_goal tasks under folder :folder:`uwlab_tasks.tasks.manipulation.track_goal` +* update track_goal tasks under folder :mod:`uwlab_tasks.tasks.manipulation.track_goal` 0.4.0 (2024-07-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ -* renaming :folder:`uwlab_tasks.tasks.manipulation.lift_cube` as - :folder:`uwlab_tasks.tasks.manipulation.lift_objects` +* renaming :mod:`uwlab_tasks.tasks.manipulation.lift_cube` as + :mod:`uwlab_tasks.tasks.manipulation.lift_objects` * separates lift_cube and lift_multiobjects as two different environments * adopting new environment structure for task lift_objects 0.3.0 (2024-07-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ -* renaming :folder:`uwlab_tasks.tasks.manipulation.craneberryLavaChocoCake` as - :folder:`uwlab_tasks.tasks.manipulation.cake_decoration` +* renaming :mod:`uwlab_tasks.tasks.manipulation.craneberryLavaChocoCake` as + :mod:`uwlab_tasks.tasks.manipulation.cake_decoration` * adopting new environment structure for task cake_decoration 0.2.3 (2024-07-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ * sketched Fetching as a separate locomotion task, instead of being a part of - :folder:`uwlab_tasks.tasks.locomotion.velocity` + :mod:`uwlab_tasks.tasks.locomotion.velocity` 0.2.2 (2024-07-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ -* dropped dependency of :folder:`uwlab_tasks.cfg` in favor of extension ``uwlab_assets`` +* dropped dependency of :mod:`uwlab_tasks.cfg` in favor of extension ``uwlab_assets`` 0.2.1 (2024-07-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -321,7 +363,7 @@ Changed * added UW as author and maintainer to :file:`uwlab_tasks.setup.py` 0.2.0 (2024-07-14) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -331,7 +373,7 @@ Changed 0.2.0 (2024-07-14) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -343,7 +385,7 @@ Changed reward_fingers_object_distance tanh return std was 0.1 now 0.2 0.1.9 (2024-07-13) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -353,11 +395,11 @@ Changed * added leap hand xarm event :func:`uwlab_tasks.cfgs.robots.leap_hand_xarm.mdp.events.reset_joints_by_offset` which accepts additional joint ids * changed cube lift environment cube size to be a bit larger -* added mass randomization cfg in cube lift environment :field:`uwlab_tasks.tasks.manipulation.lift_cube.` +* added mass randomization cfg in cube lift environment ``uwlab_tasks.tasks.manipulation.lift_cube`` 0.1.8 (2024-07-12) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -369,7 +411,7 @@ Changed 0.1.7 (2024-07-08) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -387,7 +429,7 @@ Added 0.1.6 (2024-07-07) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ memo: ^^^^^ @@ -403,9 +445,9 @@ Changed ^^^^^^^ * Changed :class:`uwlab_tasks.cfgs.robots.hebi.robot_dynamics.RobotTerminationsCfg` to include DoneTerm: robot_extremely_bad_posture -* Changed :function:`uwlab_tasks.cfgs.robots.hebi.mdp.terminations.terminate_extremely_bad_posture` to be probabilistic -* Changed :field:`uwlab_tasks.tasks.manipulation.track_goal.config.hebi.Hebi_JointPos_GoalTracking_Env.RewardsCfg.end_effector_position_tracking` - and :field:`uwlab_tasks.tasks.manipulation.track_goal.config.hebi.Hebi_JointPos_GoalTracking_Env.RewardsCfg.end_effector_orientation_tracking` +* Changed :func:`uwlab_tasks.cfgs.robots.hebi.mdp.terminations.terminate_extremely_bad_posture` to be probabilistic +* Changed :attr:`uwlab_tasks.tasks.manipulation.track_goal.config.hebi.Hebi_JointPos_GoalTracking_Env.RewardsCfg.end_effector_position_tracking` + and :attr:`uwlab_tasks.tasks.manipulation.track_goal.config.hebi.Hebi_JointPos_GoalTracking_Env.RewardsCfg.end_effector_orientation_tracking` to be incentive reward instead of punishment reward. * Renamed orbit_mdp to lab_mdp in :file:`uwlab_tasks.tasks.manipulation.track_goal.config.Hebi_JointPos_GoalTracking_Env` @@ -417,7 +459,7 @@ Added * Added experiments :file:`uwlab_tasks.tasks.manipulation.track_goal.config.hebi.strategy4_scale_experiments.py` 0.1.5 (2024-07-06) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added @@ -435,19 +477,19 @@ Added 0.1.4 (2024-07-05) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ -* :const:`uwlab_tasks.cfgs.robots.hebi.robot_cfg.HEBI_STRATEGY3_CFG` - :const:`uwlab_tasks.cfgs.robots.hebi.robot_cfg.HEBI_STRATEGY4_CFG` +* :data:`uwlab_tasks.cfgs.robots.hebi.robot_cfg.HEBI_STRATEGY3_CFG` + :data:`uwlab_tasks.cfgs.robots.hebi.robot_cfg.HEBI_STRATEGY4_CFG` changed from manually editing scaling factor to cfg specifying scaling factor. -* :const:`uwlab_tasks.cfgs.robots.hebi.robot_cfg.robot_dynamic` +* :data:`uwlab_tasks.cfgs.robots.hebi.robot_cfg.robot_dynamic` * :func:`workflows.teleoperation.teleop_se3_agent_absolute.main` added visualization for full gloves data 0.1.3 (2024-06-29) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed ^^^^^^^ @@ -459,7 +501,7 @@ Changed 0.1.2 (2024-06-28) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changed @@ -472,7 +514,7 @@ Changed 0.1.1 (2024-06-27) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ @@ -481,7 +523,7 @@ Added 0.1.0 (2024-06-11) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ diff --git a/source/uwlab_tasks/setup.py b/source/uwlab_tasks/setup.py index fc668c2..10edf66 100644 --- a/source/uwlab_tasks/setup.py +++ b/source/uwlab_tasks/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,6 +6,8 @@ """Installation script for the 'uwlab_tasks' python package.""" import os +import platform +import sys import toml from setuptools import setup @@ -18,6 +20,23 @@ # Minimum dependencies required prior to installation INSTALL_REQUIRES = [] +is_linux_x86_64 = platform.system() == "Linux" and platform.machine() in ("x86_64", "AMD64") +py = f"cp{sys.version_info.major}{sys.version_info.minor}" + +wheel_by_py = { + "cp311": ( + "https://github.com/MiroPsota/torch_packages_builder/releases/download/pytorch3d-0.7.8/" + "pytorch3d-0.7.8%2Bpt2.7.0cu128-cp311-cp311-linux_x86_64.whl" + ), + "cp310": ( + "https://github.com/MiroPsota/torch_packages_builder/releases/download/pytorch3d-0.7.8/" + "pytorch3d-0.7.8%2Bpt2.7.0cu128-cp310-cp310-linux_x86_64.whl" + ), +} + +if is_linux_x86_64 and py in wheel_by_py: + INSTALL_REQUIRES.append(f"pytorch3d @ {wheel_by_py[py]}") + # Installation operation setup( name="uwlab_tasks", diff --git a/source/uwlab_tasks/test/env_test_utils.py b/source/uwlab_tasks/test/env_test_utils.py new file mode 100644 index 0000000..342748b --- /dev/null +++ b/source/uwlab_tasks/test/env_test_utils.py @@ -0,0 +1,290 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Shared test utilities for UW Lab environments.""" + +import gymnasium as gym +import inspect +import os +import torch + +import carb +import omni.usd +import pytest +from isaaclab.envs.utils.spaces import sample_space +from isaaclab_tasks.utils.parse_cfg import parse_env_cfg +from isaacsim.core.version import get_version + + +def setup_environment( + include_play: bool = False, + factory_envs: bool | None = None, + multi_agent: bool | None = None, +) -> list[str]: + """ + Acquire all registered Isaac environment task IDs with optional filters. + + Args: + include_play: If True, include environments ending in 'Play-v0'. + factory_envs: + - True: include only Factory environments + - False: exclude Factory environments + - None: include both Factory and non-Factory environments + multi_agent: + - True: include only multi-agent environments + - False: include only single-agent environments + - None: include all environments regardless of agent type + + Returns: + A sorted list of task IDs matching the selected filters. + """ + # disable interactive mode for wandb for automate environments + os.environ["WANDB_DISABLED"] = "true" + + # acquire all Isaac environment names + registered_tasks = [] + for task_spec in gym.registry.values(): + # only consider Isaac environments + if not ("UW" in task_spec.id or "UW" in task_spec.id or "OmniReset" in task_spec.id): + continue + + # skip Deployment environments + if "Deploy" in task_spec.id: + continue + + # skip PegInsert because somehow the asset loading has issue with running with test_environments.rst + # but it is fine if run normal rl scripts + if "PegInsert" in task_spec.id: + continue + + # filter Play environments, if needed + if not include_play and task_spec.id.endswith("Play-v0"): + continue + + # TODO: factory environments cause tests to fail if run together with other envs, + # so we collect these environments separately to run in a separate unit test. + # apply factory filter + if (factory_envs is True and ("Factory" not in task_spec.id and "Forge" not in task_spec.id)) or ( + factory_envs is False and ("Factory" in task_spec.id or "Forge" in task_spec.id) + ): + continue + # if None: no filter + + # apply multi agent filter + if multi_agent is not None: + # parse config + env_cfg = parse_env_cfg(task_spec.id) + if (multi_agent is True and not hasattr(env_cfg, "possible_agents")) or ( + multi_agent is False and hasattr(env_cfg, "possible_agents") + ): + continue + # if None: no filter + + registered_tasks.append(task_spec.id) + + # sort environments alphabetically + registered_tasks.sort() + + # this flag is necessary to prevent a bug where the simulation gets stuck randomy when running many environments + carb.settings.get_settings().set_bool("/physics/cooking/ujitsoCollisionCooking", False) + + print(">>> All registered environments:", registered_tasks) + + return registered_tasks + + +def _run_environments( + task_name, + device, + num_envs, + num_steps=100, + multi_agent=False, + create_stage_in_memory=False, + disable_clone_in_fabric=False, +): + """Run all environments and check environments return valid signals. + + Args: + task_name: Name of the environment. + device: Device to use (e.g., 'cuda'). + num_envs: Number of environments. + num_steps: Number of simulation steps. + multi_agent: Whether the environment is multi-agent. + create_stage_in_memory: Whether to create stage in memory. + disable_clone_in_fabric: Whether to disable fabric cloning. + """ + + # 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: + 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 + if "Suction" in task_name and device != "cpu": + return + + # skip these environments as they cannot be run with 32 environments within reasonable VRAM + if num_envs == 32 and task_name in [ + "Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-v0", + "Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0", + "Isaac-Stack-Cube-Instance-Randomize-Franka-v0", + ]: + return + + # skip these environments as they cannot be run with 32 environments within reasonable VRAM + if "Visuomotor" in task_name and num_envs == 32: + return + + # skip automate environments as they require cuda installation + if task_name in ["Isaac-AutoMate-Assembly-Direct-v0", "Isaac-AutoMate-Disassembly-Direct-v0"]: + return + + # Check if this is the teddy bear environment and if it's being called from the right test file + if task_name == "Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0": + # Get the calling frame to check which test file is calling this function + frame = inspect.currentframe() + while frame: + filename = frame.f_code.co_filename + if "test_lift_teddy_bear.py" in filename: + # Called from the dedicated test file, allow it to run + break + frame = frame.f_back + + # If not called from the dedicated test file, skip it + if not frame: + return + + print(f""">>> Running test for environment: {task_name}""") + _check_random_actions( + task_name, + device, + num_envs, + num_steps=num_steps, + multi_agent=multi_agent, + create_stage_in_memory=create_stage_in_memory, + disable_clone_in_fabric=disable_clone_in_fabric, + ) + print(f""">>> Closing environment: {task_name}""") + print("-" * 80) + + +def _check_random_actions( + task_name: str, + device: str, + num_envs: int, + num_steps: int = 100, + multi_agent: bool = False, + create_stage_in_memory: bool = False, + disable_clone_in_fabric: bool = False, +): + """Run random actions and check environments return valid signals. + + Args: + task_name: Name of the environment. + device: Device to use (e.g., 'cuda'). + num_envs: Number of environments. + num_steps: Number of simulation steps. + multi_agent: Whether the environment is multi-agent. + create_stage_in_memory: Whether to create stage in memory. + disable_clone_in_fabric: Whether to disable fabric cloning. + """ + # create a new context stage, if stage in memory is not enabled + if not create_stage_in_memory: + 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: + # parse config + env_cfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) + # set config args + env_cfg.sim.create_stage_in_memory = create_stage_in_memory + if disable_clone_in_fabric: + env_cfg.scene.clone_in_fabric = False + + # filter based off multi agents mode and create env + if multi_agent: + if not hasattr(env_cfg, "possible_agents"): + print(f"[INFO]: Skipping {task_name} as it is not a multi-agent task") + return + env = gym.make(task_name, cfg=env_cfg) + else: + if hasattr(env_cfg, "possible_agents"): + print(f"[INFO]: Skipping {task_name} as it is a multi-agent task") + return + env = gym.make(task_name, 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 {task_name}. Error: {e}") + + # disable control on stop + env.unwrapped.sim._app_control_on_stop_handle = None # type: ignore + + # override action space if set to inf for `Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0` + if task_name == "Isaac-Lift-Teddy-Bear-Franka-IK-Abs-v0": + for i in range(env.unwrapped.single_action_space.shape[0]): + if env.unwrapped.single_action_space.low[i] == float("-inf"): + env.unwrapped.single_action_space.low[i] = -1.0 + if env.unwrapped.single_action_space.high[i] == float("inf"): + env.unwrapped.single_action_space.low[i] = 1.0 + + # reset environment + obs, _ = env.reset() + + # check signal + assert _check_valid_tensor(obs) + + # simulate environment for num_steps + with torch.inference_mode(): + for _ in range(num_steps): + # sample actions according to the defined space + if multi_agent: + actions = { + agent: sample_space( + env.unwrapped.action_spaces[agent], device=env.unwrapped.device, batch_size=num_envs + ) + for agent in env.unwrapped.possible_agents + } + else: + actions = sample_space( + env.unwrapped.single_action_space, device=env.unwrapped.device, batch_size=num_envs + ) + # apply actions + transition = env.step(actions) + # check signals + for data in transition[:-1]: # exclude info + if multi_agent: + for agent, agent_data in data.items(): + assert _check_valid_tensor(agent_data), f"Invalid data ('{agent}'): {agent_data}" + else: + assert _check_valid_tensor(data), f"Invalid data: {data}" + + # close environment + env.close() + + +def _check_valid_tensor(data: torch.Tensor | dict) -> bool: + """Checks if given data does not have corrupted values. + + Args: + data: Data buffer. + + Returns: + True if the data is valid. + """ + if isinstance(data, torch.Tensor): + return not torch.any(torch.isnan(data)) + elif isinstance(data, (tuple, list)): + return all(_check_valid_tensor(value) for value in data) + elif isinstance(data, dict): + return all(_check_valid_tensor(value) for value in data.values()) + else: + raise ValueError(f"Input data of invalid type: {type(data)}.") diff --git a/source/uwlab_tasks/test/test_environment_determinism.py b/source/uwlab_tasks/test/test_environment_determinism.py new file mode 100644 index 0000000..de9f6e4 --- /dev/null +++ b/source/uwlab_tasks/test/test_environment_determinism.py @@ -0,0 +1,128 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Launch Isaac Sim Simulator first.""" + +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 torch + +import carb +import omni.usd +import pytest +from isaaclab_tasks.utils.parse_cfg import parse_env_cfg + +import uwlab_tasks # noqa: F401 + + +@pytest.fixture(scope="module", autouse=True) +def setup_environment(): + # this flag is necessary to prevent a bug where the simulation gets stuck randomly when running the + # test on many environments. + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/physics/cooking/ujitsoCollisionCooking", False) + + +@pytest.mark.parametrize( + "task_name", + [ + "UW-TrackGoal-XarmLeap-JointPos-v0", + "UW-Track-Goal-Ur5-JointPos-v0", + ], +) +@pytest.mark.parametrize("device", ["cuda", "cpu"]) +def test_manipulation_env_determinism(task_name, device): + """Check deterministic environment creation for manipulation.""" + _test_environment_determinism(task_name, device) + + +@pytest.mark.parametrize( + "task_name", + [ + "UW-Position-Advance-Skills-Spot-v0", + "UW-Position-Stepping-Stone-Spot-v0", + "UW-Velocity-Rough-Spot-v0", + ], +) +@pytest.mark.parametrize("device", ["cuda", "cpu"]) +def test_locomotion_env_determinism(task_name, device): + """Check deterministic environment creation for locomotion.""" + _test_environment_determinism(task_name, device) + + +@pytest.mark.parametrize( + "task_name", + [ + "Isaac-Repose-Cube-Allegro-v0", + # "Isaac-Repose-Cube-Allegro-Direct-v0", # FIXME: @kellyg, any idea why it is not deterministic? + ], +) +@pytest.mark.parametrize("device", ["cuda", "cpu"]) +def test_dextrous_env_determinism(task_name, device): + """Check deterministic environment creation for dextrous manipulation.""" + _test_environment_determinism(task_name, device) + + +def _test_environment_determinism(task_name: str, device: str): + """Check deterministic environment creation.""" + # fix number of steps + num_envs = 32 + num_steps = 100 + # call function to create and step the environment + obs_1, rew_1 = _obtain_transition_tuples(task_name, num_envs, device, num_steps) + obs_2, rew_2 = _obtain_transition_tuples(task_name, num_envs, device, num_steps) + + # check everything is as expected + # -- rewards should be the same + torch.testing.assert_close(rew_1, rew_2) + # -- observations should be the same + for key in obs_1.keys(): + torch.testing.assert_close(obs_1[key], obs_2[key]) + + +def _obtain_transition_tuples(task_name: str, num_envs: int, device: str, num_steps: int) -> tuple[dict, torch.Tensor]: + """Run random actions and obtain transition tuples after fixed number of steps.""" + # create a new stage + omni.usd.get_context().new_stage() + try: + # parse configuration + env_cfg = parse_env_cfg(task_name, device=device, num_envs=num_envs) + # set seed + env_cfg.seed = 42 + # create environment + env = gym.make(task_name, cfg=env_cfg) + except Exception as e: + if "env" in locals() and hasattr(env, "_is_closed"): + env.close() + else: + if hasattr(e, "obj") and hasattr(e.obj, "_is_closed"): + e.obj.close() + pytest.fail(f"Failed to set-up the environment for task {task_name}. Error: {e}") + + # disable control on stop + env.unwrapped.sim._app_control_on_stop_handle = None # type: ignore + + # reset environment + obs, _ = env.reset() + # simulate environment for fixed steps + with torch.inference_mode(): + for _ in range(num_steps): + # sample actions from -1 to 1 + actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1 + # apply actions and get initial observation + obs, rewards = env.step(actions)[:2] + + # close the environment + env.close() + + return obs, rewards diff --git a/source/uwlab_tasks/test/test_environments.py b/source/uwlab_tasks/test/test_environments.py new file mode 100644 index 0000000..43f0087 --- /dev/null +++ b/source/uwlab_tasks/test/test_environments.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Launch Isaac Sim Simulator first.""" + + +from isaaclab.app import AppLauncher + +# launch the simulator +app_launcher = AppLauncher(headless=True, enable_cameras=True) +simulation_app = app_launcher.app + + +"""Rest everything follows.""" + +import pytest +from env_test_utils import _run_environments, setup_environment + +import uwlab_tasks # noqa: F401 + + +@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)) +@pytest.mark.isaacsim_ci +def test_environments(task_name, num_envs, device): + # run environments without stage in memory + _run_environments(task_name, device, num_envs, create_stage_in_memory=False) diff --git a/source/uwlab_tasks/test/test_hydra.py b/source/uwlab_tasks/test/test_hydra.py new file mode 100644 index 0000000..f3f75cc --- /dev/null +++ b/source/uwlab_tasks/test/test_hydra.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch the simulator +app_launcher = AppLauncher(headless=True) +simulation_app = app_launcher.app + + +"""Rest everything follows.""" + +import hydra +import isaaclab_tasks # noqa: F401 +import pytest + +import uwlab_tasks # noqa: F401 +from uwlab_tasks.utils.hydra import hydra_task_compose + + +@pytest.mark.parametrize("override", ["env.scene.terrain=gap", "env.scene.terrain=all"]) +def test_hydra_group_override(override: str): + """Test the hydra configuration system for group overriding behavior with two choices.""" + + @hydra_task_compose("UW-Position-Advance-Skills-Spot-v0", "rsl_rl_cfg_entry_point", [override]) + def main(env_cfg, agent_cfg): + keys = list(env_cfg.scene.terrain.terrain_generator.sub_terrains.keys()) + if override.endswith("=gap"): + assert len(keys) == 1 + assert keys[0] == "gap" + else: + # full set should contain multiple terrains including 'gap' + assert len(keys) > 1 + assert "gap" in keys + + main() + # clean up + hydra.core.global_hydra.GlobalHydra.instance().clear() diff --git a/source/uwlab_tasks/uwlab_tasks/__init__.py b/source/uwlab_tasks/uwlab_tasks/__init__.py index 6e24c56..9c2b8e9 100644 --- a/source/uwlab_tasks/uwlab_tasks/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/__init__.py b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/__init__.py new file mode 100644 index 0000000..f2bb7c4 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/__init__.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Cartpole balancing environment. +""" + +import gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +gym.register( + id="Isaac-Cartpole-Direct-v0", + entry_point=f"{__name__}.cartpole_env:CartpoleEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_env:CartpoleEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CartpolePPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + "sb3_cfg_entry_point": f"{agents.__name__}:sb3_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Cartpole-RGB-Camera-Direct-v0", + entry_point=f"{__name__}.cartpole_camera_env:CartpoleCameraEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env:CartpoleRGBCameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_camera_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Cartpole-Depth-Camera-Direct-v0", + entry_point=f"{__name__}.cartpole_camera_env:CartpoleCameraEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env:CartpoleDepthCameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_camera_ppo_cfg.yaml", + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rl_games_camera_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rl_games_camera_ppo_cfg.yaml new file mode 100644 index 0000000..6f31c56 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rl_games_camera_ppo_cfg.yaml @@ -0,0 +1,100 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +params: + seed: 42 + + # environment wrapper clipping + env: + # added to the wrapper + clip_observations: 5.0 + # can make custom wrapper? + clip_actions: 1.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + # doesn't have this fine grained control but made it close + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + cnn: + type: conv2d + activation: relu + initializer: + name: default + regularizer: + name: None + convs: + - filters: 32 + kernel_size: 8 + strides: 4 + padding: 0 + - filters: 64 + kernel_size: 4 + strides: 2 + padding: 0 + - filters: 64 + kernel_size: 3 + strides: 1 + padding: 0 + + mlp: + units: [512] + activation: elu + initializer: + name: default + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: cartpole_camera_direct + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: False + normalize_value: True + num_actors: -1 # configured from the script (based on num_envs) + reward_shaper: + scale_value: 1.0 + normalize_advantage: True + gamma: 0.99 + tau : 0.95 + learning_rate: 1e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 500 + save_best_after: 50 + save_frequency: 25 + grad_norm: 1.0 + entropy_coef: 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 64 + minibatch_size: 2048 + mini_epochs: 4 + critic_coef: 2 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rl_games_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 0000000..0947666 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,83 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +params: + seed: 42 + + # environment wrapper clipping + env: + # added to the wrapper + clip_observations: 5.0 + # can make custom wrapper? + clip_actions: 1.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + # doesn't have this fine grained control but made it close + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [32, 32] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: cartpole_direct + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: True + normalize_value: True + num_actors: -1 # configured from the script (based on num_envs) + reward_shaper: + scale_value: 0.1 + normalize_advantage: True + gamma: 0.99 + tau : 0.95 + learning_rate: 5e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 150 + save_best_after: 50 + save_frequency: 25 + grad_norm: 1.0 + entropy_coef: 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 32 + minibatch_size: 16384 + mini_epochs: 8 + critic_coef: 4 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rsl_rl_ppo_cfg.py b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 0000000..b7d28f5 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class CartpolePPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 16 + max_iterations = 150 + save_interval = 50 + experiment_name = "cartpole_direct" + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_obs_normalization=False, + critic_obs_normalization=False, + actor_hidden_dims=[32, 32], + critic_hidden_dims=[32, 32], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.005, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/sb3_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/sb3_ppo_cfg.yaml new file mode 100644 index 0000000..269e9f3 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/sb3_ppo_cfg.yaml @@ -0,0 +1,25 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Reference: https://github.com/DLR-RM/rl-baselines3-zoo/blob/master/hyperparams/ppo.yml#L32 +seed: 42 + +n_timesteps: !!float 1e6 +policy: 'MlpPolicy' +n_steps: 16 +batch_size: 4096 +gae_lambda: 0.95 +gamma: 0.99 +n_epochs: 20 +ent_coef: 0.01 +learning_rate: !!float 3e-4 +clip_range: !!float 0.2 +policy_kwargs: + activation_fn: 'nn.ELU' + net_arch: [32, 32] + squash_output: False +vf_coef: 1.0 +max_grad_norm: 1.0 +device: "cuda:0" diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/skrl_camera_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/skrl_camera_ppo_cfg.yaml new file mode 100644 index 0000000..d8a2d04 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/skrl_camera_ppo_cfg.yaml @@ -0,0 +1,101 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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: features_extractor + input: permute(OBSERVATIONS, (0, 3, 1, 2)) # PyTorch NHWC -> NCHW. Warning: don't permute for JAX since it expects NHWC + layers: + - conv2d: {out_channels: 32, kernel_size: 8, stride: 4, padding: 0} + - conv2d: {out_channels: 64, kernel_size: 4, stride: 2, padding: 0} + - conv2d: {out_channels: 64, kernel_size: 3, stride: 1, padding: 0} + - flatten + activations: relu + - name: net + input: features_extractor + layers: [512] + activations: elu + output: ACTIONS + value: # see deterministic_model parameters + class: DeterministicMixin + clip_actions: False + network: + - name: features_extractor + input: permute(OBSERVATIONS, (0, 3, 1, 2)) # PyTorch NHWC -> NCHW. Warning: don't permute for JAX since it expects NHWC + layers: + - conv2d: {out_channels: 32, kernel_size: 8, stride: 4, padding: 0} + - conv2d: {out_channels: 64, kernel_size: 4, stride: 2, padding: 0} + - conv2d: {out_channels: 64, kernel_size: 3, stride: 1, padding: 0} + - flatten + activations: relu + - name: net + input: features_extractor + layers: [512] + 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: 64 + learning_epochs: 4 + mini_batches: 32 + discount_factor: 0.99 + lambda: 0.95 + learning_rate: 1.0e-04 + learning_rate_scheduler: KLAdaptiveLR + learning_rate_scheduler_kwargs: + kl_threshold: 0.008 + 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.0 + value_loss_scale: 1.0 + kl_threshold: 0.0 + rewards_shaper_scale: 1.0 + time_limit_bootstrap: False + # logging and checkpoint + experiment: + directory: "cartpole_camera_direct" + experiment_name: "" + write_interval: auto + checkpoint_interval: auto + + +# Sequential trainer +# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html +trainer: + class: SequentialTrainer + timesteps: 32000 + environment_info: log diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/skrl_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/skrl_ppo_cfg.yaml new file mode 100644 index 0000000..356ca28 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/agents/skrl_ppo_cfg.yaml @@ -0,0 +1,85 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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: OBSERVATIONS + layers: [32, 32] + activations: elu + output: ACTIONS + value: # see deterministic_model parameters + class: DeterministicMixin + clip_actions: False + network: + - name: net + input: OBSERVATIONS + layers: [32, 32] + 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: 32 + learning_epochs: 8 + mini_batches: 8 + discount_factor: 0.99 + lambda: 0.95 + learning_rate: 5.0e-04 + learning_rate_scheduler: KLAdaptiveLR + learning_rate_scheduler_kwargs: + kl_threshold: 0.008 + 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.0 + value_loss_scale: 2.0 + kl_threshold: 0.0 + rewards_shaper_scale: 0.1 + time_limit_bootstrap: False + # logging and checkpoint + experiment: + directory: "cartpole_direct" + experiment_name: "" + write_interval: auto + checkpoint_interval: auto + + +# Sequential trainer +# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html +trainer: + class: SequentialTrainer + timesteps: 4800 + environment_info: log diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_camera_env.py b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_camera_env.py new file mode 100644 index 0000000..caa96d6 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_camera_env.py @@ -0,0 +1,227 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import math +import torch +from collections.abc import Sequence + +import isaaclab.sim as sim_utils +from isaaclab.assets import Articulation, ArticulationCfg +from isaaclab.envs import DirectRLEnv, DirectRLEnvCfg, ViewerCfg +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sensors import TiledCamera, TiledCameraCfg, save_images_to_file +from isaaclab.sim import SimulationCfg +from isaaclab.utils import configclass +from isaaclab.utils.math import sample_uniform +from isaaclab_assets.robots.cartpole import CARTPOLE_CFG + + +@configclass +class CartpoleRGBCameraEnvCfg(DirectRLEnvCfg): + # env + decimation = 2 + episode_length_s = 5.0 + action_scale = 100.0 # [N] + + # simulation + sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) + + # robot + robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="/World/envs/env_.*/Robot") + cart_dof_name = "slider_to_cart" + pole_dof_name = "cart_to_pole" + + # camera + tiled_camera: TiledCameraCfg = TiledCameraCfg( + prim_path="/World/envs/env_.*/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(-5.0, 0.0, 2.0), rot=(1.0, 0.0, 0.0, 0.0), convention="world"), + data_types=["rgb"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) + ), + width=100, + height=100, + ) + write_image_to_file = False + + # spaces + action_space = 1 + state_space = 0 + observation_space = [tiled_camera.height, tiled_camera.width, 3] + + # change viewer settings + viewer = ViewerCfg(eye=(20.0, 20.0, 20.0)) + + # scene + scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=512, env_spacing=20.0, replicate_physics=True) + + # reset + max_cart_pos = 3.0 # the cart is reset if it exceeds that position [m] + initial_pole_angle_range = [-0.125, 0.125] # the range in which the pole angle is sampled from on reset [rad] + + # reward scales + rew_scale_alive = 1.0 + rew_scale_terminated = -2.0 + rew_scale_pole_pos = -1.0 + rew_scale_cart_vel = -0.01 + rew_scale_pole_vel = -0.005 + + +@configclass +class CartpoleDepthCameraEnvCfg(CartpoleRGBCameraEnvCfg): + # camera + tiled_camera: TiledCameraCfg = TiledCameraCfg( + prim_path="/World/envs/env_.*/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(-5.0, 0.0, 2.0), rot=(1.0, 0.0, 0.0, 0.0), convention="world"), + data_types=["depth"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) + ), + width=100, + height=100, + ) + + # spaces + observation_space = [tiled_camera.height, tiled_camera.width, 1] + + +class CartpoleCameraEnv(DirectRLEnv): + + cfg: CartpoleRGBCameraEnvCfg | CartpoleDepthCameraEnvCfg + + def __init__( + self, cfg: CartpoleRGBCameraEnvCfg | CartpoleDepthCameraEnvCfg, render_mode: str | None = None, **kwargs + ): + super().__init__(cfg, render_mode, **kwargs) + + self._cart_dof_idx, _ = self._cartpole.find_joints(self.cfg.cart_dof_name) + self._pole_dof_idx, _ = self._cartpole.find_joints(self.cfg.pole_dof_name) + self.action_scale = self.cfg.action_scale + + self.joint_pos = self._cartpole.data.joint_pos + self.joint_vel = self._cartpole.data.joint_vel + + if len(self.cfg.tiled_camera.data_types) != 1: + raise ValueError( + "The Cartpole camera environment only supports one image type at a time but the following were" + f" provided: {self.cfg.tiled_camera.data_types}" + ) + + def close(self): + """Cleanup for the environment.""" + super().close() + + def _setup_scene(self): + """Setup the scene with the cartpole and camera.""" + self._cartpole = Articulation(self.cfg.robot_cfg) + self._tiled_camera = TiledCamera(self.cfg.tiled_camera) + + # clone and replicate + self.scene.clone_environments(copy_from_source=False) + if self.device == "cpu": + # we need to explicitly filter collisions for CPU simulation + self.scene.filter_collisions(global_prim_paths=[]) + + # add articulation and sensors to scene + self.scene.articulations["cartpole"] = self._cartpole + self.scene.sensors["tiled_camera"] = self._tiled_camera + # add lights + light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) + light_cfg.func("/World/Light", light_cfg) + + def _pre_physics_step(self, actions: torch.Tensor) -> None: + self.actions = self.action_scale * actions.clone() + + def _apply_action(self) -> None: + self._cartpole.set_joint_effort_target(self.actions, joint_ids=self._cart_dof_idx) + + def _get_observations(self) -> dict: + data_type = "rgb" if "rgb" in self.cfg.tiled_camera.data_types else "depth" + if "rgb" in self.cfg.tiled_camera.data_types: + camera_data = self._tiled_camera.data.output[data_type] / 255.0 + # normalize the camera data for better training results + mean_tensor = torch.mean(camera_data, dim=(1, 2), keepdim=True) + camera_data -= mean_tensor + elif "depth" in self.cfg.tiled_camera.data_types: + camera_data = self._tiled_camera.data.output[data_type] + camera_data[camera_data == float("inf")] = 0 + observations = {"policy": camera_data.clone()} + + if self.cfg.write_image_to_file: + save_images_to_file(observations["policy"], f"cartpole_{data_type}.png") + + return observations + + def _get_rewards(self) -> torch.Tensor: + total_reward = compute_rewards( + self.cfg.rew_scale_alive, + self.cfg.rew_scale_terminated, + self.cfg.rew_scale_pole_pos, + self.cfg.rew_scale_cart_vel, + self.cfg.rew_scale_pole_vel, + self.joint_pos[:, self._pole_dof_idx[0]], + self.joint_vel[:, self._pole_dof_idx[0]], + self.joint_pos[:, self._cart_dof_idx[0]], + self.joint_vel[:, self._cart_dof_idx[0]], + self.reset_terminated, + ) + return total_reward + + def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: + self.joint_pos = self._cartpole.data.joint_pos + self.joint_vel = self._cartpole.data.joint_vel + + time_out = self.episode_length_buf >= self.max_episode_length - 1 + out_of_bounds = torch.any(torch.abs(self.joint_pos[:, self._cart_dof_idx]) > self.cfg.max_cart_pos, dim=1) + out_of_bounds = out_of_bounds | torch.any(torch.abs(self.joint_pos[:, self._pole_dof_idx]) > math.pi / 2, dim=1) + return out_of_bounds, time_out + + def _reset_idx(self, env_ids: Sequence[int] | None): + if env_ids is None: + env_ids = self._cartpole._ALL_INDICES + super()._reset_idx(env_ids) + + joint_pos = self._cartpole.data.default_joint_pos[env_ids] + joint_pos[:, self._pole_dof_idx] += sample_uniform( + self.cfg.initial_pole_angle_range[0] * math.pi, + self.cfg.initial_pole_angle_range[1] * math.pi, + joint_pos[:, self._pole_dof_idx].shape, + joint_pos.device, + ) + joint_vel = self._cartpole.data.default_joint_vel[env_ids] + + default_root_state = self._cartpole.data.default_root_state[env_ids] + default_root_state[:, :3] += self.scene.env_origins[env_ids] + + self.joint_pos[env_ids] = joint_pos + self.joint_vel[env_ids] = joint_vel + + self._cartpole.write_root_pose_to_sim(default_root_state[:, :7], env_ids) + self._cartpole.write_root_velocity_to_sim(default_root_state[:, 7:], env_ids) + self._cartpole.write_joint_state_to_sim(joint_pos, joint_vel, None, env_ids) + + +@torch.jit.script +def compute_rewards( + rew_scale_alive: float, + rew_scale_terminated: float, + rew_scale_pole_pos: float, + rew_scale_cart_vel: float, + rew_scale_pole_vel: float, + pole_pos: torch.Tensor, + pole_vel: torch.Tensor, + cart_pos: torch.Tensor, + cart_vel: torch.Tensor, + reset_terminated: torch.Tensor, +): + rew_alive = rew_scale_alive * (1.0 - reset_terminated.float()) + rew_termination = rew_scale_terminated * reset_terminated.float() + rew_pole_pos = rew_scale_pole_pos * torch.sum(torch.square(pole_pos).unsqueeze(dim=1), dim=-1) + rew_cart_vel = rew_scale_cart_vel * torch.sum(torch.abs(cart_vel).unsqueeze(dim=1), dim=-1) + rew_pole_vel = rew_scale_pole_vel * torch.sum(torch.abs(pole_vel).unsqueeze(dim=1), dim=-1) + total_reward = rew_alive + rew_termination + rew_pole_pos + rew_cart_vel + rew_pole_vel + return total_reward diff --git a/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_env.py b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_env.py new file mode 100644 index 0000000..5b90ef6 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/cartpole/cartpole_env.py @@ -0,0 +1,173 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import math +import torch +from collections.abc import Sequence + +import isaaclab.sim as sim_utils +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.spawners.from_files import GroundPlaneCfg, spawn_ground_plane +from isaaclab.utils import configclass +from isaaclab.utils.math import sample_uniform +from isaaclab_assets.robots.cartpole import CARTPOLE_CFG + + +@configclass +class CartpoleEnvCfg(DirectRLEnvCfg): + # env + decimation = 2 + episode_length_s = 5.0 + action_scale = 100.0 # [N] + action_space = 1 + observation_space = 4 + state_space = 0 + + # simulation + sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) + + # robot + robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="/World/envs/env_.*/Robot") + cart_dof_name = "slider_to_cart" + pole_dof_name = "cart_to_pole" + + # scene + scene: InteractiveSceneCfg = InteractiveSceneCfg( + num_envs=4096, env_spacing=4.0, replicate_physics=True, clone_in_fabric=True + ) + + # reset + max_cart_pos = 3.0 # the cart is reset if it exceeds that position [m] + initial_pole_angle_range = [-0.25, 0.25] # the range in which the pole angle is sampled from on reset [rad] + + # reward scales + rew_scale_alive = 1.0 + rew_scale_terminated = -2.0 + rew_scale_pole_pos = -1.0 + rew_scale_cart_vel = -0.01 + rew_scale_pole_vel = -0.005 + + +class CartpoleEnv(DirectRLEnv): + cfg: CartpoleEnvCfg + + def __init__(self, cfg: CartpoleEnvCfg, render_mode: str | None = None, **kwargs): + super().__init__(cfg, render_mode, **kwargs) + + self._cart_dof_idx, _ = self.cartpole.find_joints(self.cfg.cart_dof_name) + self._pole_dof_idx, _ = self.cartpole.find_joints(self.cfg.pole_dof_name) + self.action_scale = self.cfg.action_scale + + self.joint_pos = self.cartpole.data.joint_pos + self.joint_vel = self.cartpole.data.joint_vel + + def _setup_scene(self): + self.cartpole = Articulation(self.cfg.robot_cfg) + # add ground plane + spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg()) + # clone and replicate + self.scene.clone_environments(copy_from_source=False) + # we need to explicitly filter collisions for CPU simulation + if self.device == "cpu": + self.scene.filter_collisions(global_prim_paths=[]) + # add articulation to scene + self.scene.articulations["cartpole"] = self.cartpole + # add lights + light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) + light_cfg.func("/World/Light", light_cfg) + + def _pre_physics_step(self, actions: torch.Tensor) -> None: + self.actions = self.action_scale * actions.clone() + + def _apply_action(self) -> None: + self.cartpole.set_joint_effort_target(self.actions, joint_ids=self._cart_dof_idx) + + def _get_observations(self) -> dict: + obs = torch.cat( + ( + self.joint_pos[:, self._pole_dof_idx[0]].unsqueeze(dim=1), + self.joint_vel[:, self._pole_dof_idx[0]].unsqueeze(dim=1), + self.joint_pos[:, self._cart_dof_idx[0]].unsqueeze(dim=1), + self.joint_vel[:, self._cart_dof_idx[0]].unsqueeze(dim=1), + ), + dim=-1, + ) + observations = {"policy": obs} + return observations + + def _get_rewards(self) -> torch.Tensor: + total_reward = compute_rewards( + self.cfg.rew_scale_alive, + self.cfg.rew_scale_terminated, + self.cfg.rew_scale_pole_pos, + self.cfg.rew_scale_cart_vel, + self.cfg.rew_scale_pole_vel, + self.joint_pos[:, self._pole_dof_idx[0]], + self.joint_vel[:, self._pole_dof_idx[0]], + self.joint_pos[:, self._cart_dof_idx[0]], + self.joint_vel[:, self._cart_dof_idx[0]], + self.reset_terminated, + ) + return total_reward + + def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: + self.joint_pos = self.cartpole.data.joint_pos + self.joint_vel = self.cartpole.data.joint_vel + + time_out = self.episode_length_buf >= self.max_episode_length - 1 + out_of_bounds = torch.any(torch.abs(self.joint_pos[:, self._cart_dof_idx]) > self.cfg.max_cart_pos, dim=1) + out_of_bounds = out_of_bounds | torch.any(torch.abs(self.joint_pos[:, self._pole_dof_idx]) > math.pi / 2, dim=1) + return out_of_bounds, time_out + + def _reset_idx(self, env_ids: Sequence[int] | None): + if env_ids is None: + env_ids = self.cartpole._ALL_INDICES + super()._reset_idx(env_ids) + + joint_pos = self.cartpole.data.default_joint_pos[env_ids] + joint_pos[:, self._pole_dof_idx] += sample_uniform( + self.cfg.initial_pole_angle_range[0] * math.pi, + self.cfg.initial_pole_angle_range[1] * math.pi, + joint_pos[:, self._pole_dof_idx].shape, + joint_pos.device, + ) + joint_vel = self.cartpole.data.default_joint_vel[env_ids] + + default_root_state = self.cartpole.data.default_root_state[env_ids] + default_root_state[:, :3] += self.scene.env_origins[env_ids] + + self.joint_pos[env_ids] = joint_pos + self.joint_vel[env_ids] = joint_vel + + self.cartpole.write_root_pose_to_sim(default_root_state[:, :7], env_ids) + self.cartpole.write_root_velocity_to_sim(default_root_state[:, 7:], env_ids) + self.cartpole.write_joint_state_to_sim(joint_pos, joint_vel, None, env_ids) + + +@torch.jit.script +def compute_rewards( + rew_scale_alive: float, + rew_scale_terminated: float, + rew_scale_pole_pos: float, + rew_scale_cart_vel: float, + rew_scale_pole_vel: float, + pole_pos: torch.Tensor, + pole_vel: torch.Tensor, + cart_pos: torch.Tensor, + cart_vel: torch.Tensor, + reset_terminated: torch.Tensor, +): + rew_alive = rew_scale_alive * (1.0 - reset_terminated.float()) + rew_termination = rew_scale_terminated * reset_terminated.float() + rew_pole_pos = rew_scale_pole_pos * torch.sum(torch.square(pole_pos).unsqueeze(dim=1), dim=-1) + rew_cart_vel = rew_scale_cart_vel * torch.sum(torch.abs(cart_vel).unsqueeze(dim=1), dim=-1) + rew_pole_vel = rew_scale_pole_vel * torch.sum(torch.abs(pole_vel).unsqueeze(dim=1), dim=-1) + total_reward = rew_alive + rew_termination + rew_pole_pos + rew_cart_vel + rew_pole_vel + return total_reward diff --git a/source/uwlab_tasks/uwlab_tasks/direct/humanoid/__init__.py b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/__init__.py new file mode 100644 index 0000000..c590c58 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Humanoid locomotion environment. +""" + +import gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +gym.register( + id="Isaac-Humanoid-Direct-v0", + entry_point=f"{__name__}.humanoid_env:HumanoidEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.humanoid_env:HumanoidEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HumanoidPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/rl_games_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 0000000..627679c --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,80 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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: [400, 200, 100] + 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: humanoid_direct + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: True + normalize_input: True + normalize_value: True + value_bootstrap: True + num_actors: -1 # configured from the script (based on num_envs) + reward_shaper: + scale_value: 0.01 + normalize_advantage: True + gamma: 0.99 + tau: 0.95 + learning_rate: 5e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 1000 + save_best_after: 100 + save_frequency: 50 + grad_norm: 1.0 + entropy_coef: 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 32 + minibatch_size: 32768 + mini_epochs: 5 + critic_coef: 4 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/rsl_rl_ppo_cfg.py b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 0000000..b50b29f --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class HumanoidPPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 32 + max_iterations = 1000 + save_interval = 50 + experiment_name = "humanoid_direct" + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + 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, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.0, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-4, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.008, + max_grad_norm=1.0, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/skrl_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/skrl_ppo_cfg.yaml new file mode 100644 index 0000000..26dbf50 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/agents/skrl_ppo_cfg.yaml @@ -0,0 +1,85 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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: OBSERVATIONS + layers: [400, 200, 100] + activations: elu + output: ACTIONS + value: # see deterministic_model parameters + class: DeterministicMixin + clip_actions: False + network: + - name: net + input: OBSERVATIONS + layers: [400, 200, 100] + 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: 32 + learning_epochs: 5 + mini_batches: 4 + discount_factor: 0.99 + lambda: 0.95 + learning_rate: 5.0e-04 + learning_rate_scheduler: KLAdaptiveLR + learning_rate_scheduler_kwargs: + kl_threshold: 0.008 + 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.0 + value_loss_scale: 2.0 + kl_threshold: 0.0 + rewards_shaper_scale: 0.01 + time_limit_bootstrap: False + # logging and checkpoint + experiment: + directory: "humanoid_direct" + experiment_name: "" + write_interval: auto + checkpoint_interval: auto + + +# Sequential trainer +# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html +trainer: + class: SequentialTrainer + timesteps: 32000 + environment_info: log diff --git a/source/uwlab_tasks/uwlab_tasks/direct/humanoid/humanoid_env.py b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/humanoid_env.py new file mode 100644 index 0000000..e400682 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/direct/humanoid/humanoid_env.py @@ -0,0 +1,95 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg +from isaaclab.envs import DirectRLEnvCfg +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sim import SimulationCfg +from isaaclab.terrains import TerrainImporterCfg +from isaaclab.utils import configclass +from isaaclab_assets import HUMANOID_CFG +from isaaclab_tasks.direct.locomotion.locomotion_env import LocomotionEnv + + +@configclass +class HumanoidEnvCfg(DirectRLEnvCfg): + # env + episode_length_s = 15.0 + decimation = 2 + action_scale = 1.0 + action_space = 21 + observation_space = 75 + state_space = 0 + + # simulation + sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation) + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="average", + restitution_combine_mode="average", + static_friction=1.0, + dynamic_friction=1.0, + restitution=0.0, + ), + debug_vis=False, + ) + + # scene + scene: InteractiveSceneCfg = InteractiveSceneCfg( + num_envs=4096, env_spacing=4.0, replicate_physics=True, clone_in_fabric=True + ) + + # robot + robot: ArticulationCfg = HUMANOID_CFG.replace(prim_path="/World/envs/env_.*/Robot") + joint_gears: list = [ + 67.5000, # lower_waist + 67.5000, # lower_waist + 67.5000, # right_upper_arm + 67.5000, # right_upper_arm + 67.5000, # left_upper_arm + 67.5000, # left_upper_arm + 67.5000, # pelvis + 45.0000, # right_lower_arm + 45.0000, # left_lower_arm + 45.0000, # right_thigh: x + 135.0000, # right_thigh: y + 45.0000, # right_thigh: z + 45.0000, # left_thigh: x + 135.0000, # left_thigh: y + 45.0000, # left_thigh: z + 90.0000, # right_knee + 90.0000, # left_knee + 22.5, # right_foot + 22.5, # right_foot + 22.5, # left_foot + 22.5, # left_foot + ] + + heading_weight: float = 0.5 + up_weight: float = 0.1 + + energy_cost_scale: float = 0.05 + actions_cost_scale: float = 0.01 + alive_reward_scale: float = 2.0 + dof_vel_scale: float = 0.1 + + death_cost: float = -1.0 + termination_height: float = 0.8 + + angular_velocity_scale: float = 0.25 + contact_force_scale: float = 0.01 + + +class HumanoidEnv(LocomotionEnv): + cfg: HumanoidEnvCfg + + def __init__(self, cfg: HumanoidEnvCfg, render_mode: str | None = None, **kwargs): + super().__init__(cfg, render_mode, **kwargs) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/__init__.py index b25784a..e6b67af 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/__init__.py new file mode 100644 index 0000000..b4978fb --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/__init__.py @@ -0,0 +1,70 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Cartpole balancing environment. +""" + +import gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +gym.register( + id="Isaac-Cartpole-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_env_cfg:CartpoleEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CartpolePPORunnerCfg", + "rsl_rl_with_symmetry_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:CartpolePPORunnerWithSymmetryCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + "sb3_cfg_entry_point": f"{agents.__name__}:sb3_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Cartpole-RGB-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleRGBCameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Cartpole-Depth-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleDepthCameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_camera_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Cartpole-RGB-ResNet18-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleResNet18CameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_feature_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Cartpole-RGB-TheiaTiny-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.cartpole_camera_env_cfg:CartpoleTheiaTinyCameraEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_feature_ppo_cfg.yaml", + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_camera_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_camera_ppo_cfg.yaml new file mode 100644 index 0000000..c6a1b90 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_camera_ppo_cfg.yaml @@ -0,0 +1,100 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +params: + seed: 42 + + # environment wrapper clipping + env: + # added to the wrapper + clip_observations: 5.0 + # can make custom wrapper? + clip_actions: 1.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + # doesn't have this fine grained control but made it close + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + cnn: + type: conv2d + activation: relu + initializer: + name: default + regularizer: + name: None + convs: + - filters: 32 + kernel_size: 8 + strides: 4 + padding: 0 + - filters: 64 + kernel_size: 4 + strides: 2 + padding: 0 + - filters: 64 + kernel_size: 3 + strides: 1 + padding: 0 + + mlp: + units: [512] + activation: elu + initializer: + name: default + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: cartpole_camera + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: False + normalize_value: True + num_actors: -1 # configured from the script (based on num_envs) + reward_shaper: + scale_value: 1.0 + normalize_advantage: True + gamma: 0.99 + tau : 0.95 + learning_rate: 1e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 500 + save_best_after: 50 + save_frequency: 25 + grad_norm: 1.0 + entropy_coef: 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 64 + minibatch_size: 2048 + mini_epochs: 4 + critic_coef: 2 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml new file mode 100644 index 0000000..9efaf3f --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_feature_ppo_cfg.yaml @@ -0,0 +1,84 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +params: + seed: 42 + + # environment wrapper clipping + env: + # added to the wrapper + clip_observations: 5.0 + # can make custom wrapper? + clip_actions: 1.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + # doesn't have this fine grained control but made it close + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [256] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: cartpole_features + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: True + normalize_value: True + value_bootstrap: True + num_actors: -1 # configured from the script (based on num_envs) + reward_shaper: + scale_value: 1.0 + normalize_advantage: True + gamma: 0.99 + tau : 0.95 + learning_rate: 3e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 200 + save_best_after: 50 + save_frequency: 25 + grad_norm: 1.0 + entropy_coef: 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 16 + minibatch_size: 2048 + mini_epochs: 8 + critic_coef: 4 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 0000000..f29e4b2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,83 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +params: + seed: 42 + + # environment wrapper clipping + env: + # added to the wrapper + clip_observations: 5.0 + # can make custom wrapper? + clip_actions: 1.0 + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + # doesn't have this fine grained control but made it close + network: + name: actor_critic + separate: False + space: + continuous: + mu_activation: None + sigma_activation: None + + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [32, 32] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: cartpole + 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.0 + normalize_advantage: False + gamma: 0.99 + tau : 0.95 + learning_rate: 3e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 150 + save_best_after: 50 + save_frequency: 25 + grad_norm: 1.0 + entropy_coef: 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 16 + minibatch_size: 8192 + mini_epochs: 8 + critic_coef: 4 + clip_value: True + seq_length: 4 + bounds_loss_coef: 0.0001 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rsl_rl_ppo_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 0000000..6e16049 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,62 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import isaaclab_tasks.manager_based.classic.cartpole.mdp.symmetry as symmetry +from isaaclab.utils import configclass +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg, RslRlSymmetryCfg + + +@configclass +class CartpolePPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 16 + max_iterations = 150 + save_interval = 50 + experiment_name = "cartpole" + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_obs_normalization=False, + critic_obs_normalization=False, + actor_hidden_dims=[32, 32], + critic_hidden_dims=[32, 32], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.005, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) + + +@configclass +class CartpolePPORunnerWithSymmetryCfg(CartpolePPORunnerCfg): + """Configuration for the PPO agent with symmetry augmentation.""" + + # all the other settings are inherited from the parent class + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.005, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + symmetry_cfg=RslRlSymmetryCfg( + use_data_augmentation=True, data_augmentation_func=symmetry.compute_symmetric_states + ), + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/sb3_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/sb3_ppo_cfg.yaml new file mode 100644 index 0000000..269e9f3 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/sb3_ppo_cfg.yaml @@ -0,0 +1,25 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Reference: https://github.com/DLR-RM/rl-baselines3-zoo/blob/master/hyperparams/ppo.yml#L32 +seed: 42 + +n_timesteps: !!float 1e6 +policy: 'MlpPolicy' +n_steps: 16 +batch_size: 4096 +gae_lambda: 0.95 +gamma: 0.99 +n_epochs: 20 +ent_coef: 0.01 +learning_rate: !!float 3e-4 +clip_range: !!float 0.2 +policy_kwargs: + activation_fn: 'nn.ELU' + net_arch: [32, 32] + squash_output: False +vf_coef: 1.0 +max_grad_norm: 1.0 +device: "cuda:0" diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/skrl_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/skrl_ppo_cfg.yaml new file mode 100644 index 0000000..5e457fc --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/agents/skrl_ppo_cfg.yaml @@ -0,0 +1,85 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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: OBSERVATIONS + layers: [32, 32] + activations: elu + output: ACTIONS + value: # see deterministic_model parameters + class: DeterministicMixin + clip_actions: False + network: + - name: net + input: OBSERVATIONS + layers: [32, 32] + 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: 16 + learning_epochs: 8 + mini_batches: 8 + discount_factor: 0.99 + lambda: 0.95 + learning_rate: 3.0e-04 + learning_rate_scheduler: KLAdaptiveLR + learning_rate_scheduler_kwargs: + kl_threshold: 0.008 + state_preprocessor: null + state_preprocessor_kwargs: null + value_preprocessor: null + 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.0 + value_loss_scale: 2.0 + kl_threshold: 0.0 + rewards_shaper_scale: 1.0 + time_limit_bootstrap: False + # logging and checkpoint + experiment: + directory: "cartpole" + experiment_name: "" + write_interval: auto + checkpoint_interval: auto + + +# Sequential trainer +# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html +trainer: + class: SequentialTrainer + timesteps: 2400 + environment_info: log diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py new file mode 100644 index 0000000..c859cd0 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_camera_env_cfg.py @@ -0,0 +1,175 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import isaaclab.sim as sim_utils +import isaaclab_tasks.manager_based.classic.cartpole.mdp as mdp +from isaaclab.managers import ObservationGroupCfg as ObsGroup +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import TiledCameraCfg +from isaaclab.utils import configclass + +from .cartpole_env_cfg import CartpoleEnvCfg, CartpoleSceneCfg + +## +# Scene definition +## + + +@configclass +class CartpoleRGBCameraSceneCfg(CartpoleSceneCfg): + + # add camera to the scene + tiled_camera: TiledCameraCfg = TiledCameraCfg( + prim_path="{ENV_REGEX_NS}/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(-7.0, 0.0, 3.0), rot=(0.9945, 0.0, 0.1045, 0.0), convention="world"), + data_types=["rgb"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) + ), + width=100, + height=100, + ) + + +@configclass +class CartpoleDepthCameraSceneCfg(CartpoleSceneCfg): + + # add camera to the scene + tiled_camera: TiledCameraCfg = TiledCameraCfg( + prim_path="{ENV_REGEX_NS}/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(-7.0, 0.0, 3.0), rot=(0.9945, 0.0, 0.1045, 0.0), convention="world"), + data_types=["distance_to_camera"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) + ), + width=100, + height=100, + ) + + +## +# MDP settings +## + + +@configclass +class RGBObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class RGBCameraPolicyCfg(ObsGroup): + """Observations for policy group with RGB images.""" + + image = ObsTerm(func=mdp.image, params={"sensor_cfg": SceneEntityCfg("tiled_camera"), "data_type": "rgb"}) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + policy: ObsGroup = RGBCameraPolicyCfg() + + +@configclass +class DepthObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class DepthCameraPolicyCfg(ObsGroup): + """Observations for policy group with depth images.""" + + image = ObsTerm( + func=mdp.image, params={"sensor_cfg": SceneEntityCfg("tiled_camera"), "data_type": "distance_to_camera"} + ) + + policy: ObsGroup = DepthCameraPolicyCfg() + + +@configclass +class ResNet18ObservationCfg: + """Observation specifications for the MDP.""" + + @configclass + class ResNet18FeaturesCameraPolicyCfg(ObsGroup): + """Observations for policy group with features extracted from RGB images with a frozen ResNet18.""" + + image = ObsTerm( + func=mdp.image_features, + params={"sensor_cfg": SceneEntityCfg("tiled_camera"), "data_type": "rgb", "model_name": "resnet18"}, + ) + + policy: ObsGroup = ResNet18FeaturesCameraPolicyCfg() + + +@configclass +class TheiaTinyObservationCfg: + """Observation specifications for the MDP.""" + + @configclass + class TheiaTinyFeaturesCameraPolicyCfg(ObsGroup): + """Observations for policy group with features extracted from RGB images with a frozen Theia-Tiny Transformer""" + + image = ObsTerm( + func=mdp.image_features, + params={ + "sensor_cfg": SceneEntityCfg("tiled_camera"), + "data_type": "rgb", + "model_name": "theia-tiny-patch16-224-cddsv", + "model_device": "cuda:0", + }, + ) + + policy: ObsGroup = TheiaTinyFeaturesCameraPolicyCfg() + + +## +# Environment configuration +## + + +@configclass +class CartpoleRGBCameraEnvCfg(CartpoleEnvCfg): + """Configuration for the cartpole environment with RGB camera.""" + + scene: CartpoleRGBCameraSceneCfg = CartpoleRGBCameraSceneCfg(num_envs=512, env_spacing=20) + observations: RGBObservationsCfg = RGBObservationsCfg() + + def __post_init__(self): + super().__post_init__() + # remove ground as it obstructs the camera + self.scene.ground = None + # viewer settings + self.viewer.eye = (7.0, 0.0, 2.5) + self.viewer.lookat = (0.0, 0.0, 2.5) + + +@configclass +class CartpoleDepthCameraEnvCfg(CartpoleEnvCfg): + """Configuration for the cartpole environment with depth camera.""" + + scene: CartpoleDepthCameraSceneCfg = CartpoleDepthCameraSceneCfg(num_envs=512, env_spacing=20) + observations: DepthObservationsCfg = DepthObservationsCfg() + + def __post_init__(self): + super().__post_init__() + # remove ground as it obstructs the camera + self.scene.ground = None + # viewer settings + self.viewer.eye = (7.0, 0.0, 2.5) + self.viewer.lookat = (0.0, 0.0, 2.5) + + +@configclass +class CartpoleResNet18CameraEnvCfg(CartpoleRGBCameraEnvCfg): + """Configuration for the cartpole environment with ResNet18 features as observations.""" + + observations: ResNet18ObservationCfg = ResNet18ObservationCfg() + + +@configclass +class CartpoleTheiaTinyCameraEnvCfg(CartpoleRGBCameraEnvCfg): + """Configuration for the cartpole environment with Theia-Tiny features as observations.""" + + observations: TheiaTinyObservationCfg = TheiaTinyObservationCfg() diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py new file mode 100644 index 0000000..b649d2e --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py @@ -0,0 +1,180 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import math + +import isaaclab.sim as sim_utils +import isaaclab_tasks.manager_based.classic.cartpole.mdp as mdp +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.utils import configclass + +## +# Pre-defined configs +## +from isaaclab_assets.robots.cartpole import CARTPOLE_CFG # isort:skip + + +## +# Scene definition +## + + +@configclass +class CartpoleSceneCfg(InteractiveSceneCfg): + """Configuration for a cart-pole scene.""" + + # ground plane + ground = AssetBaseCfg( + prim_path="/World/ground", + spawn=sim_utils.GroundPlaneCfg(size=(100.0, 100.0)), + ) + + # cartpole + robot: ArticulationCfg = CARTPOLE_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/DomeLight", + spawn=sim_utils.DomeLightCfg(color=(0.9, 0.9, 0.9), intensity=500.0), + ) + + +## +# MDP settings +## + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + joint_effort = mdp.JointEffortActionCfg(asset_name="robot", joint_names=["slider_to_cart"], scale=100.0) + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + # observation terms (order preserved) + joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel) + joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel) + + def __post_init__(self) -> None: + self.enable_corruption = False + self.concatenate_terms = True + + # observation groups + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + # reset + reset_cart_position = EventTerm( + func=mdp.reset_joints_by_offset, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"]), + "position_range": (-1.0, 1.0), + "velocity_range": (-0.5, 0.5), + }, + ) + + reset_pole_position = EventTerm( + func=mdp.reset_joints_by_offset, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"]), + "position_range": (-0.25 * math.pi, 0.25 * math.pi), + "velocity_range": (-0.25 * math.pi, 0.25 * math.pi), + }, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + # (1) Constant running reward + alive = RewTerm(func=mdp.is_alive, weight=1.0) + # (2) Failure penalty + terminating = RewTerm(func=mdp.is_terminated, weight=-2.0) + # (3) Primary task: keep pole upright + pole_pos = RewTerm( + func=mdp.joint_pos_target_l2, + weight=-1.0, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"]), "target": 0.0}, + ) + # (4) Shaping tasks: lower cart velocity + cart_vel = RewTerm( + func=mdp.joint_vel_l1, + weight=-0.01, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"])}, + ) + # (5) Shaping tasks: lower pole angular velocity + pole_vel = RewTerm( + func=mdp.joint_vel_l1, + weight=-0.005, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["cart_to_pole"])}, + ) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + # (1) Time out + time_out = DoneTerm(func=mdp.time_out, time_out=True) + # (2) Cart out of bounds + cart_out_of_bounds = DoneTerm( + func=mdp.joint_pos_out_of_manual_limit, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["slider_to_cart"]), "bounds": (-3.0, 3.0)}, + ) + + +## +# Environment configuration +## + + +@configclass +class CartpoleEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the cartpole environment.""" + + # Scene settings + scene: CartpoleSceneCfg = CartpoleSceneCfg(num_envs=4096, env_spacing=4.0, clone_in_fabric=True) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + events: EventCfg = EventCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + + # Post initialization + def __post_init__(self) -> None: + """Post initialization.""" + # general settings + self.decimation = 2 + self.episode_length_s = 5 + # viewer settings + self.viewer.eye = (8.0, 0.0, 5.0) + # simulation settings + self.sim.dt = 1 / 120 + self.sim.render_interval = self.decimation diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/__init__.py new file mode 100644 index 0000000..9ad60f0 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This sub-module contains the functions that are specific to the cartpole environments.""" + +from isaaclab.envs.mdp import * # noqa: F401, F403 + +from .rewards import * # noqa: F401, F403 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/rewards.py new file mode 100644 index 0000000..7c9cfaa --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/rewards.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +from isaaclab.assets import Articulation +from isaaclab.managers import SceneEntityCfg +from isaaclab.utils.math import wrap_to_pi + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def joint_pos_target_l2(env: ManagerBasedRLEnv, target: float, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Penalize joint position deviation from a target value.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # wrap the joint positions to (-pi, pi) + joint_pos = wrap_to_pi(asset.data.joint_pos[:, asset_cfg.joint_ids]) + # compute the reward + return torch.sum(torch.square(joint_pos - target), dim=1) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/symmetry.py b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/symmetry.py new file mode 100644 index 0000000..877af63 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/classic/cartpole/mdp/symmetry.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Functions to specify the symmetry in the observation and action space for cartpole.""" + +from __future__ import annotations + +import torch +from tensordict import TensorDict +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from omni.isaac.lab.envs import ManagerBasedRLEnv + +# specify the functions that are available for import +__all__ = ["compute_symmetric_states"] + + +@torch.no_grad() +def compute_symmetric_states( + env: ManagerBasedRLEnv, + obs: TensorDict | None = None, + actions: torch.Tensor | None = None, +): + """Augments the given observations and actions by applying symmetry transformations. + + This function creates augmented versions of the provided observations and actions by applying + two symmetrical transformations: original, left-right. The symmetry + transformations are beneficial for reinforcement learning tasks by providing additional + diverse data without requiring additional data collection. + + Args: + env: The environment instance. + obs: The original observation tensor dictionary. Defaults to None. + actions: The original actions tensor. Defaults to None. + + Returns: + Augmented observations and actions tensors, or None if the respective input was None. + """ + + # observations + if obs is not None: + batch_size = obs.batch_size[0] + # since we have 2 different symmetries, we need to augment the batch size by 2 + obs_aug = obs.repeat(2) + # -- original + obs_aug["policy"][:batch_size] = obs["policy"][:] + # -- left-right + obs_aug["policy"][batch_size : 2 * batch_size] = -obs["policy"] + else: + obs_aug = None + + # actions + if actions is not None: + batch_size = actions.shape[0] + # since we have 4 different symmetries, we need to augment the batch size by 4 + actions_aug = torch.zeros(batch_size * 2, actions.shape[1], device=actions.device) + # -- original + actions_aug[:batch_size] = actions[:] + # -- left-right + actions_aug[batch_size : 2 * batch_size] = -actions + else: + actions_aug = None + + return obs_aug, actions_aug diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/__init__.py index bcd1f3c..2c250d9 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/__init__.py index 1eb3dbe..e496ddd 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py index 4660c33..00cd4cb 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_base_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -7,7 +7,7 @@ import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg -from isaaclab.envs import ViewerCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg from isaaclab.managers import CurriculumTermCfg as CurrTerm from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationGroupCfg as ObsGroup @@ -15,16 +15,17 @@ 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 ContactSensorCfg, RayCasterCfg, patterns +from isaaclab.terrains import TerrainGeneratorCfg, TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise -from uwlab.envs.data_manager_based_rl import DataManagerBasedRLEnvCfg -from uwlab.scene import InteractiveSceneCfg -from uwlab.terrains import TerrainGeneratorCfg, TerrainImporterCfg import uwlab_tasks.manager_based.locomotion.advance_skills.mdp as mdp +from .terrains import EXTREME_STAIR, GAP, IRREGULAR_PILLAR_OBSTACLE, PIT, SLOPE_INV, SQUARE_PILLAR_OBSTACLE + @configclass class SceneCfg(InteractiveSceneCfg): @@ -77,7 +78,7 @@ class SceneCfg(InteractiveSceneCfg): height_scanner = RayCasterCfg( prim_path="{ENV_REGEX_NS}/Robot/base", offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), - attach_yaw_only=True, + ray_alignment="yaw", pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=(1.6, 1.0)), debug_vis=False, mesh_prim_paths=["/World/ground"], @@ -97,6 +98,7 @@ class ActionsCfg: @configclass class CommandsCfg: "Command specifications for the MDP." + goal_point = mdp.TerrainBasedPose2dCommandCfg( asset_name="robot", resampling_time_range=(10.0, 10.0), @@ -274,8 +276,60 @@ class CurriculumCfg: terrain_levels = CurrTerm(func=mdp.terrain_levels_vel) # type: ignore +def make_terrain(terrain_dict): + + return TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="generator", + terrain_generator=TerrainGeneratorCfg( + size=(10.0, 10.0), + border_width=20.0, + num_rows=10, + num_cols=20, + horizontal_scale=0.1, + vertical_scale=0.005, + slope_threshold=0.75, + use_cache=False, + sub_terrains=terrain_dict, + ), + max_init_terrain_level=5, + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + ), + visual_material=sim_utils.MdlFileCfg( + mdl_path=f"{ISAACLAB_NUCLEUS_DIR}/Materials/TilesMarbleSpiderWhiteBrickBondHoned/TilesMarbleSpiderWhiteBrickBondHoned.mdl", + project_uvw=True, + texture_scale=(0.25, 0.25), + ), + debug_vis=False, + ) + + +variants = { + "scene.terrain": { + "all": make_terrain({ + "gap": GAP, + "pit": PIT, + "extreme_stair": EXTREME_STAIR, + "slope_inv": SLOPE_INV, + "square_pillar_obstacle": SQUARE_PILLAR_OBSTACLE, + "irregular_pillar_obstacle": IRREGULAR_PILLAR_OBSTACLE, + }), + "gap": make_terrain({"gap": GAP}), + "pit": make_terrain({"pit": PIT}), + "extreme_stair": make_terrain({"extreme_stair": EXTREME_STAIR}), + "slope_inv": make_terrain({"slope_inv": SLOPE_INV}), + "square_pillar_obstacle": make_terrain({"square_pillar_obstacle": SQUARE_PILLAR_OBSTACLE}), + } +} + + @configclass -class AdvanceSkillsBaseEnvCfg(DataManagerBasedRLEnvCfg): +class AdvanceSkillsBaseEnvCfg(ManagerBasedRLEnvCfg): scene: SceneCfg = SceneCfg(num_envs=4096, env_spacing=10) observations: ObservationsCfg = ObservationsCfg() actions: ActionsCfg = ActionsCfg() @@ -285,13 +339,13 @@ class AdvanceSkillsBaseEnvCfg(DataManagerBasedRLEnvCfg): events: EventsCfg = EventsCfg() curriculum: CurriculumCfg = CurriculumCfg() viewer: ViewerCfg = ViewerCfg(eye=(1.0, 2.0, 2.0), origin_type="asset_body", asset_name="robot", body_name="base") + variants = variants def __post_init__(self): self.decimation = 4 self.episode_length_s = 6.0 self.sim.dt = 0.005 self.sim.render_interval = self.decimation - self.sim.disable_contact_processing = True self.sim.physics_material = self.scene.terrain.physics_material self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**24 self.sim.physx.gpu_found_lost_pairs_capacity = 2**24 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_env.py index 8480861..49851e5 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/advance_skills_env.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/__init__.py index b33ce65..039452e 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/__init__.py @@ -1,9 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -""" Advanced Skill Environments. +"""Advanced Skill Environments. Reference: https://github.com/leggedrobotics/legged_gym diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/__init__.py index 3dfa5ae..94c76ae 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ gym.register( id="UW-Position-Advance-Skills-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:AdvanceSkillsSpotEnvCfg", @@ -19,7 +19,7 @@ gym.register( id="UW-Position-Pit-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:PitSpotEnvCfg", @@ -29,7 +29,7 @@ gym.register( id="UW-Position-Gap-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:GapSpotEnvCfg", @@ -39,7 +39,7 @@ gym.register( id="UW-Position-Inv-Slope-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:SlopeInvSpotEnvCfg", @@ -49,7 +49,7 @@ gym.register( id="UW-Position-Extreme-Stair-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:ExtremeStairSpotEnvCfg", @@ -59,20 +59,10 @@ gym.register( id="UW-Position-Square-Obstacle-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:SquarePillarObstacleSpotEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:AdvanceSkillsSpotPPORunnerCfg", }, ) - -gym.register( - id="UW-Position-Irregular-Obstacle-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", - disable_env_checker=True, - kwargs={ - "env_cfg_entry_point": f"{__name__}.spot_env_cfg:IrregularPillarObstacleSpotEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:AdvanceSkillsSpotPPORunnerCfg", - }, -) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/rsl_rl_cfg.py index bf4684d..392bdb6 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/agents/rsl_rl_cfg.py @@ -1,12 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - -from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg -from uwlab_rl.rsl_rl import RslRlFancyPpoAlgorithmCfg, SymmetryCfg +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg, RslRlSymmetryCfg from ..augment import aug_func @@ -25,7 +23,7 @@ class AdvanceSkillsSpotPPORunnerCfg(RslRlOnPolicyRunnerCfg): critic_hidden_dims=[512, 256, 128], activation="elu", ) - algorithm = RslRlFancyPpoAlgorithmCfg( + algorithm = RslRlPpoAlgorithmCfg( value_loss_coef=1.0, use_clipped_value_loss=True, clip_param=0.2, @@ -38,5 +36,7 @@ class AdvanceSkillsSpotPPORunnerCfg(RslRlOnPolicyRunnerCfg): lam=0.95, desired_kl=0.01, max_grad_norm=1.0, - symmetry_cfg=SymmetryCfg(use_data_augmentation=True, use_mirror_loss=False, data_augmentation_func=aug_func), + symmetry_cfg=RslRlSymmetryCfg( + use_data_augmentation=True, use_mirror_loss=False, data_augmentation_func=aug_func + ), ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/augment.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/augment.py index 365c440..075034f 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/augment.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/augment.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -89,13 +89,20 @@ def aug_action(actions: torch.Tensor) -> torch.Tensor: return new_actions -def aug_func(obs=None, actions=None, env=None, is_critic=False): +def aug_func(obs=None, actions=None, env=None): aug_obs = None aug_act = None if obs is not None: - if is_critic: - aug_obs = aug_observation(obs) - aug_obs = aug_observation(obs) + aug_obs = obs.repeat(2) + if "policy" in obs: + aug_obs["policy"] = aug_observation(obs["policy"]) + elif "critic" in obs: + aug_obs["critic"] = aug_observation(obs["critic"]) + else: + raise ValueError( + "nothing is augmented because not policy or critic keyword found in tensordict, you" + f" keys: {list(obs.keys())} \n please check for potential bug" + ) if actions is not None: aug_act = aug_action(actions) return aug_obs, aug_act diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/__init__.py index 0e0c812..59f3843 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py index 9d317f6..dc57c9a 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/spot_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/spot_env_cfg.py index 8baa83b..7147260 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/spot_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot/spot_env_cfg.py @@ -1,14 +1,14 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -import uwlab_assets.robots.spot as spot - from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass +import uwlab_assets.robots.spot as spot + import uwlab_tasks.manager_based.locomotion.advance_skills.config.spot.mdp as spot_mdp from ... import advance_skills_base_env, advance_skills_env diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/__init__.py index 661ebb9..6b7cf5a 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ gym.register( id="UW-Position-Advance-Skills-Arm-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:AdvanceSkillsSpotEnvCfg", @@ -19,7 +19,7 @@ gym.register( id="UW-Position-Pit-Arm-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:PitSpotEnvCfg", @@ -29,7 +29,7 @@ gym.register( id="UW-Position-Gap-Arm-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:GapSpotEnvCfg", @@ -39,7 +39,7 @@ gym.register( id="UW-Position-Inv-Slope-Arm-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:SlopeInvSpotEnvCfg", @@ -49,7 +49,7 @@ gym.register( id="UW-Position-Extreme-Stair-Arm-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:ExtremeStairSpotEnvCfg", @@ -59,7 +59,7 @@ gym.register( id="UW-Position-Square-Obstacle-Arm-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:SquarePillarObstacleSpotEnvCfg", @@ -69,7 +69,7 @@ gym.register( id="UW-Position-Irregular-Obstacle-Arm-Spot-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.spot_env_cfg:IrregularPillarObstacleSpotEnvCfg", diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/rsl_rl_cfg.py index cb18186..492e7d8 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/agents/rsl_rl_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/spot_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/spot_env_cfg.py index c587b0f..50624fc 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/spot_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/config/spot_with_arm/spot_env_cfg.py @@ -1,14 +1,14 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -import uwlab_assets.robots.spot as spot - from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass +import uwlab_assets.robots.spot as spot + import uwlab_tasks.manager_based.locomotion.advance_skills.config.spot.mdp as spot_mdp from ... import advance_skills_base_env, advance_skills_env diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/__init__.py index 25e0278..805fb5d 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,6 +6,7 @@ """This sub-module contains the functions that are specific to the locomotion environments.""" from isaaclab.envs.mdp import * + from uwlab.envs.mdp import * # noqa: F401, F403 from .curriculums import * # noqa: F401, F403 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/curriculums.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/curriculums.py index bcd807f..b91d6e5 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/curriculums.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/curriculums.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/observations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/observations.py index 229b728..87f460b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/observations.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/observations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py index 0c66220..00bf786 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,14 +9,12 @@ import torch from isaaclab.assets import Articulation +from isaaclab.envs import ManagerBasedRLEnv from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import ContactSensor -from uwlab.envs import DataManagerBasedRLEnv -def task_reward( - env: DataManagerBasedRLEnv, reward_window: float = 1.0 # Represents Tr, the length of the reward window -): +def task_reward(env: ManagerBasedRLEnv, reward_window: float = 1.0): # Represents Tr, the length of the reward window # # See section II.B (page 3) Exploration Reward for details. # Calculate the time step at which the reward window starts @@ -39,7 +37,7 @@ def task_reward( return residue_task_reward -def heading_tracking(env: DataManagerBasedRLEnv, distance_threshold: float = 2.0, reward_window: float = 2.0): +def heading_tracking(env: ManagerBasedRLEnv, distance_threshold: float = 2.0, reward_window: float = 2.0): desired_heading = env.command_manager.get_command("goal_point")[:, 3] reward_start_step = env.max_episode_length * (1 - reward_window / env.max_episode_length_s) current_dist = env.command_manager.get_command("goal_point")[:, :2].norm(2, -1) @@ -53,7 +51,7 @@ def heading_tracking(env: DataManagerBasedRLEnv, distance_threshold: float = 2.0 return r_heading_tracking -def exploration_reward(env: DataManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): +def exploration_reward(env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): # Retrieve the robot and target data robot: Articulation = env.scene[robot_cfg.name] base_velocity = robot.data.root_lin_vel_b # Robot's current base velocity vector @@ -75,7 +73,7 @@ def exploration_reward(env: DataManagerBasedRLEnv, robot_cfg: SceneEntityCfg = S def stall_penalty( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), base_vel_threshold: float = 0.1, distance_threshold: float = 0.5, @@ -86,7 +84,7 @@ def stall_penalty( return (base_vel < base_vel_threshold) & (distance_to_goal > distance_threshold) -def illegal_contact_penalty(env: DataManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg): +def illegal_contact_penalty(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg): contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] # type: ignore net_contact_forces = contact_sensor.data.net_forces_w_history # check if any contact force exceeds the threshold @@ -96,20 +94,20 @@ def illegal_contact_penalty(env: DataManagerBasedRLEnv, threshold: float, sensor ).float() -def feet_lin_acc_l2(env: DataManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): +def feet_lin_acc_l2(env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): robot: Articulation = env.scene[robot_cfg.name] feet_acc = torch.sum(torch.square(robot.data.body_lin_acc_w[..., robot_cfg.body_ids, :]), dim=(1, 2)) return feet_acc -def feet_rot_acc_l2(env: DataManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): +def feet_rot_acc_l2(env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")): robot: Articulation = env.scene[robot_cfg.name] feet_acc = torch.sum(torch.square(robot.data.body_ang_acc_w[..., robot_cfg.body_ids, :]), dim=(1, 2)) return feet_acc def stand_penalty( - env: DataManagerBasedRLEnv, + env: ManagerBasedRLEnv, height_threshold: float, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/__init__.py index edd1266..051e3c7 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/terrain_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/terrain_cfg.py index b80a951..cb771c5 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/terrain_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/advance_skills/terrains/terrain_cfg.py @@ -1,16 +1,31 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from uwlab import terrains as terrain_cfg -from uwlab.terrains.utils import FlatPatchSamplingByRadiusCfg +import isaaclab.terrains.terrain_generator as terrain_generator +from isaaclab import terrains as terrain_cfg + +from uwlab.terrains.utils import FlatPatchSamplingByRadiusCfg, PatchSamplingCfg + + +def patched_find_flat_patches(*args, **kwargs) -> None: + patch_key = "patch_radius" + kwargs[patch_key]["patched"] = True + cfg_class = kwargs[patch_key]["cfg"] + cfg_class_args = {key: val for key, val in kwargs[patch_key].items() if key not in ["func", "cfg"]} + patch_sampling_cfg: PatchSamplingCfg = cfg_class(**cfg_class_args) + return patch_sampling_cfg.func(kwargs["wp_mesh"], kwargs["origin"], patch_sampling_cfg) + + +terrain_generator.find_flat_patches = patched_find_flat_patches + GAP = terrain_cfg.MeshGapTerrainCfg( platform_width=3.0, gap_width_range=(0.05, 1.5), proportion=0.2, - patch_sampling={ + flat_patch_sampling={ "target": FlatPatchSamplingByRadiusCfg( num_patches=10, patch_radius=0.5, @@ -24,7 +39,7 @@ platform_width=3.0, pit_depth_range=(0.05, 1.2), proportion=0.2, - patch_sampling={ + flat_patch_sampling={ "target": FlatPatchSamplingByRadiusCfg( num_patches=10, patch_radius=0.5, @@ -41,7 +56,7 @@ obstacle_width_range=(0.5, 1.5), proportion=0.2, platform_width=2, - patch_sampling={ + flat_patch_sampling={ "target": FlatPatchSamplingByRadiusCfg( num_patches=10, patch_radius=0.5, @@ -61,7 +76,7 @@ object_params_end=terrain_cfg.MeshRepeatedBoxesTerrainCfg.ObjectCfg( num_objects=10, height=6.0, size=(1.0, 1.0), max_yx_angle=0.0, degrees=True ), - patch_sampling={ + flat_patch_sampling={ "target": FlatPatchSamplingByRadiusCfg( num_patches=10, patch_radius=0.5, @@ -76,7 +91,7 @@ slope_range=(0.0, 0.9), platform_width=2.0, border_width=1.5, - patch_sampling={ + flat_patch_sampling={ "target": FlatPatchSamplingByRadiusCfg( num_patches=10, patch_radius=0.5, @@ -93,7 +108,7 @@ proportion=0.2, inverted=True, border_width=1.0, - patch_sampling={ + flat_patch_sampling={ "target": FlatPatchSamplingByRadiusCfg( num_patches=10, patch_radius=0.5, diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/__init__.py new file mode 100644 index 0000000..79df323 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Risky Terrains Environments. +Reference: + https://github.com/leggedrobotics/legged_gym +""" diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/balance_beams_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/balance_beams_env.py new file mode 100644 index 0000000..ebe2f7d --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/balance_beams_env.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.utils import configclass +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp + +from . import stepping_stones_env +from .config.terrains.terrain_cfg import BALANCE_BEAMS_CFG + + +@configclass +class BalanceBeamSceneCfg(stepping_stones_env.SteppingStoneSceneCfg): + def __post_init__(self): + self.terrain.terrain_generator = BALANCE_BEAMS_CFG + + +@configclass +class CommandsCfg: + """Commands for the MDP.""" + + # for balance-beams + target_cmd = mdp.UniformPose2dCommandCfg( + asset_name="robot", + resampling_time_range=(10, 10), + debug_vis=True, + simple_heading=False, + ranges=mdp.UniformPose2dCommandCfg.Ranges(pos_x=(3.5, 4.5), pos_y=(-0.1, 0.1), heading=(-0.785, 0.785)), + ) + + +@configclass +class ObservationsCfg: + """Observations for the MDP.""" + + @configclass + class PolicyCfg(stepping_stones_env.ObservationsCfg.PolicyCfg): + def __post_init__(self): + super().__post_init__() + self.height_scan.noise = Unoise(n_min=-0.025, n_max=0.025) + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventsCfg(stepping_stones_env.EventsCfg): + """Events for the MDP.""" + + def __post_init__(self): + self.reset_episode_length.params["episode_length_s"] = (8.0, 10.0) + self.reset_base.params["pose_range"] = {"x": (-0.5, 0.5), "y": (-0.5, 0.5), "yaw": (-3.14, 3.14)} + + +@configclass +class RewardsCfg(stepping_stones_env.RewardsCfg): + """Rewards for the MDP.""" + + # balance beam specific + aggressive_motion = RewTerm(func=mdp.aggressive_motion, weight=-5.0, params={"threshold": 1.0}) + + stand_pose = RewTerm(func=mdp.stand_pos, weight=-5.0, params={"base_height": 0.6, "tr": 1.0, "phi": 0.5, "d": 0.25}) + + def __post_init__(self): + self.position_tracking.weight = 25.0 + self.head_tracking.weight = 12.0 + self.joint_torque_limits.weight = -0.5 + self.joint_torque_limits.params["ratio"] = 0.8 + self.stand_still.params["d"] = 0.25 + + +@configclass +class BalanceBeamsLocomotionEnvCfg(stepping_stones_env.SteppingStoneLocomotionEnvCfg): + scene: BalanceBeamSceneCfg = BalanceBeamSceneCfg(num_envs=4096, env_spacing=2.5) + observations: ObservationsCfg = ObservationsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + events: EventsCfg = EventsCfg() diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/__init__.py new file mode 100644 index 0000000..2e0cd43 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for velocity-based locomotion 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/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/__init__.py new file mode 100644 index 0000000..38aa234 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import gymnasium as gym + +from . import agents + +gym.register( + id="UW-Position-Stepping-Stone-Anymal-C-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.anymal_c_env_cfg:SteppingStoneAnymalCEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsAnymalCPpoRunnerCfg", + "genome_entry_point": f"{agents.__name__}.genome_cfg:RiskyTerrainAnymalCGenomeCfg", + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/genome_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/genome_cfg.py new file mode 100644 index 0000000..ee94694 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/genome_cfg.py @@ -0,0 +1,45 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + +from uwlab.genes import GenomeCfg +from uwlab.genes.gene import gene_mdp as mdp +from uwlab.genes.gene.gene_cfg import FloatGeneCfg + +GENOMIC_MUTATION_PROFILE = { + # SCENE + # "scene.height_scanner.pattern_cfg": { + # "resolution": FloatGeneCfg( + # phase=["mutate"], mutation_func=mdp.random_float, mutation_args=(0.03, 0.1)), + # "size": FloatTupleGeneCfg( + # element_length=2, + # tuple_type="descend", + # phase=["mutate"], + # mutation_func=mdp.random_float, + # mutation_args=(1.00, 2.50), + # ) + # }, + # REWARDS + "rewards": { + ".": {"weight": FloatGeneCfg(phase=["mutate"], mutation_func=mdp.add_fraction, mutation_args=(0.3,))}, + }, + # CURRICULUM + # "curriculum.terrain_levels.params": { + # "promotion_fraction": FloatGeneCfg( + # phase=["mutate"], mutation_func=mdp.random_float, mutation_args=(0.6, 0.95)), + # "demotion_fraction": FloatGeneCfg( + # phase=["mutate"], mutation_func=mdp.random_float, mutation_args=(0.01, 0.4)) + # }, +} + + +@configclass +class RiskyTerrainAnymalCGenomeCfg(GenomeCfg): + genomic_mutation_profile: dict = GENOMIC_MUTATION_PROFILE + + genomic_constraint_profile: dict = {} + + seed: int = 32 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/rsl_rl_cfg.py new file mode 100644 index 0000000..0349df6 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/agents/rsl_rl_cfg.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg, RslRlSymmetryCfg + +from ..augment import aug_func + + +@configclass +class RiskyTerrainsAnymalCPpoRunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 48 + max_iterations = 8000 + save_interval = 400 + resume = False + experiment_name = "anymal_c_risky_terrains" + empirical_normalization = False + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_hidden_dims=[512, 256, 128], + critic_hidden_dims=[512, 256, 128], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.005, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + symmetry_cfg=RslRlSymmetryCfg( + use_data_augmentation=True, use_mirror_loss=False, data_augmentation_func=aug_func + ), + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/anymal_c_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/anymal_c_env_cfg.py new file mode 100644 index 0000000..5b2d0c4 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/anymal_c_env_cfg.py @@ -0,0 +1,28 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import isaaclab_assets.robots.anymal as anymal +from isaaclab.envs.mdp.actions.actions_cfg import JointPositionActionCfg +from isaaclab.utils import configclass + +from ... import stepping_stones_env + + +@configclass +class ActionsCfg: + actions: JointPositionActionCfg = JointPositionActionCfg( + asset_name="robot", joint_names=[".*"], scale=0.5, use_default_offset=True + ) + + +@configclass +class SteppingStoneAnymalCEnvCfg(stepping_stones_env.SteppingStoneLocomotionEnvCfg): + actions: ActionsCfg = ActionsCfg() + + def __post_init__(self): + # post init of parent + super().__post_init__() + # switch robot to anymal-c + self.scene.robot = anymal.ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/augment.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/augment.py new file mode 100644 index 0000000..6354fe6 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_c/augment.py @@ -0,0 +1,207 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch +from typing import Literal + + +def aug_observation(obs: torch.Tensor) -> torch.Tensor: + """ + obs: num_steps_per_env * num_envs // num_mini_batches, 449 + ///////////////////////////////////////// + 0: [0:3] (3,) 'base_lin_vel' + 1: [3:6] (3,) 'base_ang_vel' + 2: [6:9] (3,) 'proj_gravity' + 3: [9:13] (3,) 'concatenate_cmd' + 4: [13:14] (1,) 'time_left' + 5: [14:26] (12,) 'joint_pos' + 6: [26:38] (12,) 'joint_vel' + 7: [38:50] (12,) 'last_actions' + 8: [50:] (400,) 'height_scan' + //////////////////////////////////////// + # (LF, LH, RF, RH) x (HAA, HFE, HKE) + + """ + B = obs.shape[0] + new_obs = obs.repeat(4, 1) + # Y-symmetry Front-Back Symmetry # + # 0-2 base lin vel + new_obs[B : 2 * B, 0] = -new_obs[B : 2 * B, 0] + # 3-5 base ang vel + new_obs[B : 2 * B, 4] = -new_obs[B : 2 * B, 4] + new_obs[B : 2 * B, 5] = -new_obs[B : 2 * B, 5] + # 6-8 proj gravity + new_obs[B : 2 * B, 6] = -new_obs[B : 2 * B, 6] + # 9-12 cmd + new_obs[B : 2 * B, 9] = -new_obs[B : 2 * B, 9] + new_obs[B : 2 * B, 12] = -new_obs[B : 2 * B, 12] + # 13 time left(ignored) + + # origin -> y_symmetry + # *F_HAA -> *H_HAA + # *F_HFE -> -*H_HFE + # *F_KFE -> -*H_KFE + # *H_HAA -> *F_HAA + # *H_HFE -> -*F_HFE + # *H_KFE -> -*F_KFE + + # Original vs X-symmetry + # 00 = 'LF_HAA' 01 = 'LH_HAA' + # 01 = 'LH_HAA' 00 = 'LF_HAA' + # 02 = 'RF_HAA' 03 = 'RH_HAA' + # 03 = 'RH_HAA' 02 = 'RF_HAA' + # 04 = 'LF_HFE' 05 = -'LH_HFE' + # 05 = 'LH_HFE' 04 = -'LF_HFE' + # 06 = 'RF_HFE' 07 = -'RH_HFE' + # 07 = 'RH_HFE' 06 = -'RF_HFE' + # 08 = 'LF_KFE' 09 = -'LH_KFE' + # 09 = 'LH_KFE' 08 = -'LF_KFE' + # 10 = 'RF_KFE' 11 = -'RH_KFE' + # 11 = 'RH_KFE' 10 = -'RF_KFE' + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1], device=obs.device) + # left-right exchange state + # 13-25 joint pos + new_obs[B : 2 * B, 14:26] = new_obs[B : 2 * B, 14:26][:, new_idx] * dir_change + # 25-37 joint vel + new_obs[B : 2 * B, 26:38] = new_obs[B : 2 * B, 26:38][:, new_idx] * dir_change + # 37-49 last_action + new_obs[B : 2 * B, 38:50] = new_obs[B : 2 * B, 38:50][:, new_idx] * dir_change + # height scan, x-y order, (-length/2, -width/2) to (length/2, width/2) + new_obs[B : 2 * B, 50:] = new_obs[B : 2 * B, 50:].reshape(B, 16, 25).flip(2).flatten(1, 2) + + # X-symmetry Left Right Symmetry# + # 0-2 base lin vel + new_obs[2 * B : 3 * B, 1] = -new_obs[2 * B : 3 * B, 1] + # 3-5 base ang vel + new_obs[2 * B : 3 * B, 3] = -new_obs[2 * B : 3 * B, 3] + new_obs[2 * B : 3 * B, 5] = -new_obs[2 * B : 3 * B, 5] + # 6-8 proj gravity + new_obs[2 * B : 3 * B, 7] = -new_obs[2 * B : 3 * B, 7] + # 9-12 cmd + new_obs[2 * B : 3 * B, 10] = -new_obs[2 * B : 3 * B, 10] + new_obs[2 * B : 3 * B, 12] = -new_obs[2 * B : 3 * B, 12] + # 13 time left(ignored) + # Original vs X-symmetry + # 00 = 'LF_HAA' 02 = -'RF_HAA' + # 01 = 'LH_HAA' 03 = -'RH_HAA' + # 02 = 'RF_HAA' 00 = -'LF_HAA' + # 03 = 'RH_HAA' 01 = -'LH_HAA' + # 04 = 'LF_HFE' 06 = 'RF_HFE' + # 05 = 'LH_HFE' 07 = 'RH_HFE' + # 06 = 'RF_HFE' 04 = 'LF_HFE' + # 07 = 'RH_HFE' 05 = 'LH_HFE' + # 08 = 'LF_KFE' 10 = 'RF_KFE' + # 09 = 'LH_KFE' 11 = 'RH_KFE' + # 10 = 'RF_KFE' 08 = 'LF_KFE' + # 11 = 'RH_KFE' 09 = 'LH_KFE' + + new_idx = [2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=obs.device) + new_obs[2 * B : 3 * B, 14:26] = new_obs[2 * B : 3 * B, 14:26][:, new_idx] * dir_change + new_obs[2 * B : 3 * B, 26:38] = new_obs[2 * B : 3 * B, 26:38][:, new_idx] * dir_change + new_obs[2 * B : 3 * B, 38:50] = new_obs[2 * B : 3 * B, 38:50][:, new_idx] * dir_change + new_obs[2 * B : 3 * B, 50:] = new_obs[2 * B : 3 * B, 50:].reshape(B, 16, 25).flip(1).flatten(1, 2) + + # X-Y-symmetry # + # 0-2 base lin vel + new_obs[3 * B : 4 * B, :2] = -new_obs[3 * B : 4 * B, :2] + # 3-5 base ang vel + new_obs[3 * B : 4 * B, 3] = -new_obs[3 * B : 4 * B, 3] + new_obs[3 * B : 4 * B, 4] = -new_obs[3 * B : 4 * B, 4] + # 6-8 proj gravity + new_obs[3 * B : 4 * B, 6:8] = -new_obs[3 * B : 4 * B, 6:8] + # 9-12 cmd + new_obs[3 * B : 4 * B, 9:11] = -new_obs[3 * B : 4 * B, 9:11] + # 13 time left(ignored) + # Original vs XY-symmetry + # 00 = 'LF_HAA' 03 = -'RH_HAA' + # 01 = 'LH_HAA' 02 = -'RF_HAA' + # 02 = 'RF_HAA' 01 = -'LH_HAA' + # 03 = 'RH_HAA' 00 = -'LF_HAA' + # 04 = 'LF_HFE' 07 = -'RH_HFE' + # 05 = 'LH_HFE' 06 = -'RF_HFE' + # 06 = 'RF_HFE' 05 = -'LH_HFE' + # 07 = 'RH_HFE' 04 = -'LF_HFE' + # 08 = 'LF_KFE' 11 = -'RH_KFE' + # 09 = 'LH_KFE' 10 = -'RF_KFE' + # 10 = 'RF_KFE' 09 = -'LH_KFE' + # 11 = 'RH_KFE' 08 = -'LF_KFE' + + new_idx = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8] + dir_change = torch.tensor([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], device=obs.device) + new_obs[3 * B : 4 * B, 14:26] = new_obs[3 * B : 4 * B, 14:26][:, new_idx] * dir_change + new_obs[3 * B : 4 * B, 26:38] = new_obs[3 * B : 4 * B, 26:38][:, new_idx] * dir_change + new_obs[3 * B : 4 * B, 38:50] = new_obs[3 * B : 4 * B, 38:50][:, new_idx] * dir_change + new_obs[3 * B : 4 * B, 50:] = new_obs[3 * B : 4 * B, 50:].reshape(B, 16, 25).flip(1).flip(2).flatten(1, 2) + + return new_obs + + +def aug_actions( + actions: torch.Tensor, actions_log_prob: torch.Tensor, action_mean: torch.Tensor, action_sigma: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + new_actions = actions.repeat(4, 1) + new_actions_log_prob = actions_log_prob.repeat(4, 1) + new_action_mean = action_mean.repeat(4, 1) + new_action_sigma = action_sigma.repeat(4, 1) + B = actions.shape[0] + # Y-symmetry Front-Back Symmetry # + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + new_action_mean[B : 2 * B, :] = new_action_mean[B : 2 * B, new_idx] * dir_change + new_action_sigma[B : 2 * B, :] = new_action_sigma[B : 2 * B, new_idx] + + # X-symmetry Left Right Symmetry# + new_idx = [2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[2 * B : 3 * B, :] = new_actions[2 * B : 3 * B, new_idx] * dir_change + new_action_mean[2 * B : 3 * B, :] = new_action_mean[2 * B : 3 * B, new_idx] * dir_change + new_action_sigma[2 * B : 3 * B, :] = new_action_sigma[2 * B : 3 * B, new_idx] + + # XY-symmetry + new_idx = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8] + dir_change = torch.tensor([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[3 * B : 4 * B, :] = new_actions[3 * B : 4 * B, new_idx] * dir_change + new_action_mean[3 * B : 4 * B, :] = new_action_mean[3 * B : 4 * B, new_idx] * dir_change + new_action_sigma[3 * B : 4 * B, :] = new_action_sigma[3 * B : 4 * B, new_idx] + + return new_actions, new_actions_log_prob, new_action_mean, new_action_sigma + + +def aug_action(actions: torch.Tensor) -> torch.Tensor: + new_actions = actions.repeat(4, 1) + B = actions.shape[0] + # Y-symmetry Front-Back Symmetry # + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + + # X-symmetry Left Right Symmetry# + new_idx = [2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[2 * B : 3 * B, :] = new_actions[2 * B : 3 * B, new_idx] * dir_change + + # XY-symmetry + new_idx = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8] + dir_change = torch.tensor([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[3 * B : 4 * B, :] = new_actions[3 * B : 4 * B, new_idx] * dir_change + + return new_actions + + +def aug_func(obs=None, actions=None, env=None, obs_type=Literal["policy", "critic"]): + aug_obs = None + aug_act = None + if obs is not None: + if obs_type in ("policy", "critic"): + aug_obs = aug_observation(obs) + else: + raise ValueError(f"unsupported obs type: {obs_type}, only support `policy` or `critic`") + if actions is not None: + aug_act = aug_action(actions) + return aug_obs, aug_act diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/__init__.py new file mode 100644 index 0000000..94758d8 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/__init__.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import gymnasium as gym + +from . import agents + +gym.register( + id="UW-Position-Stepping-Stone-Anymal-D-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.anymal_d_env_cfg:SteppingStoneAnymalDEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsAnymalDPpoRunnerCfg", + }, +) + + +gym.register( + id="UW-Position-Balance-Beam-Anymal-D-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.anymal_d_env_cfg:BalanceBeamsAnymalDEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsAnymalDPpoRunnerCfg", + }, +) + +gym.register( + id="UW-Position-Stepping-Beam-Anymal-D-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.anymal_d_env_cfg:SteppingBeamsAnymalDEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsAnymalDPpoRunnerCfg", + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/agents/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/agents/rsl_rl_cfg.py new file mode 100644 index 0000000..4e7dbe9 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/agents/rsl_rl_cfg.py @@ -0,0 +1,55 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg, RslRlSymmetryCfg + +from ..augment import aug_func + + +def my_experts_observation_func(env): + return env.unwrapped.obs_buf["expert_obs"] + + +@configclass +class RiskyTerrainsAnymalDPpoRunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 48 + max_iterations = 8000 + save_interval = 400 + resume = False + experiment_name = "anymal_d_risky_terrains" + empirical_normalization = False + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_hidden_dims=[512, 256, 128], + critic_hidden_dims=[512, 256, 128], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.005, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + symmetry_cfg=RslRlSymmetryCfg( + use_data_augmentation=True, use_mirror_loss=False, data_augmentation_func=aug_func + ), + # offline_algorithm_cfg=OffPolicyAlgorithmCfg( + # behavior_cloning_cfg=BehaviorCloningCfg( + # experts_path=["logs/rsl_rl/UW-Stepping-Stone-Anymal-D-v0/2025-02-14_23-05-48/exported/policy.pt"], + # experts_observation_group_cfg="uwlab_tasks.tasks.locomotion.risky_terrains.stepping_stones_env:ObservationsCfg.PolicyCfg", + # experts_observation_func=my_experts_observation_func, + # cloning_loss_coeff=1.0, + # loss_decay=1.0 + # ) + # ) + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/anymal_d_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/anymal_d_env_cfg.py new file mode 100644 index 0000000..703e4db --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/anymal_d_env_cfg.py @@ -0,0 +1,40 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import isaaclab_assets.robots.anymal as anymal +from isaaclab.envs.mdp.actions.actions_cfg import JointPositionActionCfg +from isaaclab.utils import configclass + +from ... import balance_beams_env, stepping_beams_env, stepping_stones_env + + +@configclass +class AnymalDActionsCfg: + actions = JointPositionActionCfg(asset_name="robot", joint_names=[".*"], scale=0.5, use_default_offset=True) + + +@configclass +class AnyMalDEnvMixin: + actions: AnymalDActionsCfg = AnymalDActionsCfg() + + def __post_init__(self: stepping_stones_env.SteppingStoneLocomotionEnvCfg): + # Ensure parent classes run their setup first + super().__post_init__() # type: ignore + self.scene.robot = anymal.ANYMAL_D_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") # type: ignore + + +@configclass +class SteppingStoneAnymalDEnvCfg(AnyMalDEnvMixin, stepping_stones_env.SteppingStoneLocomotionEnvCfg): + pass + + +@configclass +class BalanceBeamsAnymalDEnvCfg(AnyMalDEnvMixin, balance_beams_env.BalanceBeamsLocomotionEnvCfg): + pass + + +@configclass +class SteppingBeamsAnymalDEnvCfg(AnyMalDEnvMixin, stepping_beams_env.SteppingBeamsLocomotionEnvCfg): + pass diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/augment.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/augment.py new file mode 100644 index 0000000..359d0a1 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/anymal_d/augment.py @@ -0,0 +1,212 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch + + +def aug_observation(obs: torch.Tensor) -> torch.Tensor: + """ + obs: num_steps_per_env * num_envs // num_mini_batches, 449 + ///////////////////////////////////////// + 0: [0:3] (3,) 'base_lin_vel' + 1: [3:6] (3,) 'base_ang_vel' + 2: [6:9] (3,) 'proj_gravity' + 3: [9:13] (3,) 'concatenate_cmd' + 4: [13:14] (1,) 'time_left' + 5: [14:26] (12,) 'joint_pos' + 6: [26:38] (12,) 'joint_vel' + 7: [38:50] (12,) 'last_actions' + 8: [50:] (400,) 'height_scan' + //////////////////////////////////////// + # (LF, LH, RF, RH) x (HAA, HFE, HKE) + + """ + B = obs.shape[0] + new_obs = obs.repeat(4, 1) + # Y-symmetry Front-Back Symmetry # + # 0-2 base lin vel + new_obs[B : 2 * B, 0] = -new_obs[B : 2 * B, 0] + # 3-5 base ang vel + new_obs[B : 2 * B, 4] = -new_obs[B : 2 * B, 4] + new_obs[B : 2 * B, 5] = -new_obs[B : 2 * B, 5] + # 6-8 proj gravity + new_obs[B : 2 * B, 6] = -new_obs[B : 2 * B, 6] + # 9-12 cmd + new_obs[B : 2 * B, 9] = -new_obs[B : 2 * B, 9] + new_obs[B : 2 * B, 12] = -new_obs[B : 2 * B, 12] + # 13 time left(ignored) + + # origin -> y_symmetry + # *F_HAA -> *H_HAA + # *F_HFE -> -*H_HFE + # *F_KFE -> -*H_KFE + # *H_HAA -> *F_HAA + # *H_HFE -> -*F_HFE + # *H_KFE -> -*F_KFE + + # Original vs X-symmetry + # 00 = 'LF_HAA' 01 = 'LH_HAA' + # 01 = 'LH_HAA' 00 = 'LF_HAA' + # 02 = 'RF_HAA' 03 = 'RH_HAA' + # 03 = 'RH_HAA' 02 = 'RF_HAA' + # 04 = 'LF_HFE' 05 = -'LH_HFE' + # 05 = 'LH_HFE' 04 = -'LF_HFE' + # 06 = 'RF_HFE' 07 = -'RH_HFE' + # 07 = 'RH_HFE' 06 = -'RF_HFE' + # 08 = 'LF_KFE' 09 = -'LH_KFE' + # 09 = 'LH_KFE' 08 = -'LF_KFE' + # 10 = 'RF_KFE' 11 = -'RH_KFE' + # 11 = 'RH_KFE' 10 = -'RF_KFE' + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1], device=obs.device) + # left-right exchange state + # 13-25 joint pos + new_obs[B : 2 * B, 14:26] = new_obs[B : 2 * B, 14:26][:, new_idx] * dir_change + # 25-37 joint vel + new_obs[B : 2 * B, 26:38] = new_obs[B : 2 * B, 26:38][:, new_idx] * dir_change + # 37-49 last_action + new_obs[B : 2 * B, 38:50] = new_obs[B : 2 * B, 38:50][:, new_idx] * dir_change + # height scan, x-y order, (-length/2, -width/2) to (length/2, width/2) + new_obs[B : 2 * B, 50:] = new_obs[B : 2 * B, 50:].reshape(B, 16, 25).flip(2).flatten(1, 2) + + # X-symmetry Left Right Symmetry# + # 0-2 base lin vel + new_obs[2 * B : 3 * B, 1] = -new_obs[2 * B : 3 * B, 1] + # 3-5 base ang vel + new_obs[2 * B : 3 * B, 3] = -new_obs[2 * B : 3 * B, 3] + new_obs[2 * B : 3 * B, 5] = -new_obs[2 * B : 3 * B, 5] + # 6-8 proj gravity + new_obs[2 * B : 3 * B, 7] = -new_obs[2 * B : 3 * B, 7] + # 9-12 cmd + new_obs[2 * B : 3 * B, 10] = -new_obs[2 * B : 3 * B, 10] + new_obs[2 * B : 3 * B, 12] = -new_obs[2 * B : 3 * B, 12] + # 13 time left(ignored) + # Original vs X-symmetry + # 00 = 'LF_HAA' 02 = -'RF_HAA' + # 01 = 'LH_HAA' 03 = -'RH_HAA' + # 02 = 'RF_HAA' 00 = -'LF_HAA' + # 03 = 'RH_HAA' 01 = -'LH_HAA' + # 04 = 'LF_HFE' 06 = 'RF_HFE' + # 05 = 'LH_HFE' 07 = 'RH_HFE' + # 06 = 'RF_HFE' 04 = 'LF_HFE' + # 07 = 'RH_HFE' 05 = 'LH_HFE' + # 08 = 'LF_KFE' 10 = 'RF_KFE' + # 09 = 'LH_KFE' 11 = 'RH_KFE' + # 10 = 'RF_KFE' 08 = 'LF_KFE' + # 11 = 'RH_KFE' 09 = 'LH_KFE' + + new_idx = [2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=obs.device) + new_obs[2 * B : 3 * B, 14:26] = new_obs[2 * B : 3 * B, 14:26][:, new_idx] * dir_change + new_obs[2 * B : 3 * B, 26:38] = new_obs[2 * B : 3 * B, 26:38][:, new_idx] * dir_change + new_obs[2 * B : 3 * B, 38:50] = new_obs[2 * B : 3 * B, 38:50][:, new_idx] * dir_change + new_obs[2 * B : 3 * B, 50:] = new_obs[2 * B : 3 * B, 50:].reshape(B, 16, 25).flip(1).flatten(1, 2) + + # X-Y-symmetry # + # 0-2 base lin vel + new_obs[3 * B : 4 * B, :2] = -new_obs[3 * B : 4 * B, :2] + # 3-5 base ang vel + new_obs[3 * B : 4 * B, 3] = -new_obs[3 * B : 4 * B, 3] + new_obs[3 * B : 4 * B, 4] = -new_obs[3 * B : 4 * B, 4] + # 6-8 proj gravity + new_obs[3 * B : 4 * B, 6:8] = -new_obs[3 * B : 4 * B, 6:8] + # 9-12 cmd + new_obs[3 * B : 4 * B, 9:11] = -new_obs[3 * B : 4 * B, 9:11] + # 13 time left(ignored) + # Original vs XY-symmetry + # 00 = 'LF_HAA' 03 = -'RH_HAA' + # 01 = 'LH_HAA' 02 = -'RF_HAA' + # 02 = 'RF_HAA' 01 = -'LH_HAA' + # 03 = 'RH_HAA' 00 = -'LF_HAA' + # 04 = 'LF_HFE' 07 = -'RH_HFE' + # 05 = 'LH_HFE' 06 = -'RF_HFE' + # 06 = 'RF_HFE' 05 = -'LH_HFE' + # 07 = 'RH_HFE' 04 = -'LF_HFE' + # 08 = 'LF_KFE' 11 = -'RH_KFE' + # 09 = 'LH_KFE' 10 = -'RF_KFE' + # 10 = 'RF_KFE' 09 = -'LH_KFE' + # 11 = 'RH_KFE' 08 = -'LF_KFE' + + new_idx = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8] + dir_change = torch.tensor([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], device=obs.device) + new_obs[3 * B : 4 * B, 14:26] = new_obs[3 * B : 4 * B, 14:26][:, new_idx] * dir_change + new_obs[3 * B : 4 * B, 26:38] = new_obs[3 * B : 4 * B, 26:38][:, new_idx] * dir_change + new_obs[3 * B : 4 * B, 38:50] = new_obs[3 * B : 4 * B, 38:50][:, new_idx] * dir_change + new_obs[3 * B : 4 * B, 50:] = new_obs[3 * B : 4 * B, 50:].reshape(B, 16, 25).flip(1).flip(2).flatten(1, 2) + + return new_obs + + +def aug_actions( + actions: torch.Tensor, actions_log_prob: torch.Tensor, action_mean: torch.Tensor, action_sigma: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + new_actions = actions.repeat(4, 1) + new_actions_log_prob = actions_log_prob.repeat(4, 1) + new_action_mean = action_mean.repeat(4, 1) + new_action_sigma = action_sigma.repeat(4, 1) + B = actions.shape[0] + # Y-symmetry Front-Back Symmetry # + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + new_action_mean[B : 2 * B, :] = new_action_mean[B : 2 * B, new_idx] * dir_change + new_action_sigma[B : 2 * B, :] = new_action_sigma[B : 2 * B, new_idx] + + # X-symmetry Left Right Symmetry# + new_idx = [2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[2 * B : 3 * B, :] = new_actions[2 * B : 3 * B, new_idx] * dir_change + new_action_mean[2 * B : 3 * B, :] = new_action_mean[2 * B : 3 * B, new_idx] * dir_change + new_action_sigma[2 * B : 3 * B, :] = new_action_sigma[2 * B : 3 * B, new_idx] + + # XY-symmetry + new_idx = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8] + dir_change = torch.tensor([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[3 * B : 4 * B, :] = new_actions[3 * B : 4 * B, new_idx] * dir_change + new_action_mean[3 * B : 4 * B, :] = new_action_mean[3 * B : 4 * B, new_idx] * dir_change + new_action_sigma[3 * B : 4 * B, :] = new_action_sigma[3 * B : 4 * B, new_idx] + + return new_actions, new_actions_log_prob, new_action_mean, new_action_sigma + + +def aug_action(actions: torch.Tensor) -> torch.Tensor: + new_actions = actions.repeat(4, 1) + B = actions.shape[0] + # Y-symmetry Front-Back Symmetry # + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + + # X-symmetry Left Right Symmetry# + new_idx = [2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[2 * B : 3 * B, :] = new_actions[2 * B : 3 * B, new_idx] * dir_change + + # XY-symmetry + new_idx = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8] + dir_change = torch.tensor([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], device=actions.device) + new_actions[3 * B : 4 * B, :] = new_actions[3 * B : 4 * B, new_idx] * dir_change + + return new_actions + + +def aug_func(obs=None, actions=None, env=None): + aug_obs = None + aug_act = None + if obs is not None: + aug_obs = obs.repeat(4) + if "policy" in obs: + aug_obs["policy"] = aug_observation(obs["policy"]) + elif "critic" in obs: + aug_obs["critic"] = aug_observation(obs["critic"]) + else: + raise ValueError( + "nothing is augmented because not policy or critic keyword found in tensordict, you" + f" keys: {list(obs.keys())} \n please check for potential bug" + ) + if actions is not None: + aug_act = aug_action(actions) + return aug_obs, aug_act diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/__init__.py similarity index 52% rename from source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/__init__.py rename to source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/__init__.py index def39f9..d90da10 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -7,38 +7,33 @@ from . import agents -""" -Leap -""" gym.register( - id="UW-TrackGoal-Leap-JointPos-v0", + id="UW-Position-Stepping-Stone-Spot-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.track_goal_leap:TrackGoalLeapJointPosition", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:Base_PPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.spot_env_cfg:SteppingStoneSpotEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsSpotPpoRunnerCfg", }, - disable_env_checker=True, ) gym.register( - id="UW-TrackGoal-Leap-IkAbs-v0", + id="UW-Position-Balance-Beam-Spot-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.track_goal_leap:TrackGoalLeapMcIkAbs", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:Base_PPORunnerCfg", - "teleop_cfg_entry_point": "uwlab_assets.robots.leap.teleop:LeapTeleopCfg", + "env_cfg_entry_point": f"{__name__}.spot_env_cfg:BalanceBeamsSpotEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsSpotPpoRunnerCfg", }, - disable_env_checker=True, ) - gym.register( - id="UW-TrackGoal-Leap-IkDel-v0", + id="UW-Position-Stepping-Beam-Spot-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.track_goal_leap:TrackGoalLeapMcIkDel", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:Base_PPORunnerCfg", + "env_cfg_entry_point": f"{__name__}.spot_env_cfg:SteppingBeamsSpotEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsSpotPpoRunnerCfg", }, - disable_env_checker=True, ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/agents/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/agents/rsl_rl_cfg.py new file mode 100644 index 0000000..c335689 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/agents/rsl_rl_cfg.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg, RslRlSymmetryCfg + +from ..augment import aug_func + + +@configclass +class RiskyTerrainsSpotPpoRunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 48 + max_iterations = 8000 + save_interval = 400 + resume = False + experiment_name = "spot_risky_terrains" + empirical_normalization = False + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_hidden_dims=[512, 256, 128], + critic_hidden_dims=[512, 256, 128], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.005, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + symmetry_cfg=RslRlSymmetryCfg( + use_data_augmentation=True, use_mirror_loss=False, data_augmentation_func=aug_func + ), + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/augment.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/augment.py new file mode 100644 index 0000000..075034f --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/augment.py @@ -0,0 +1,108 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch + + +def aug_observation(obs: torch.Tensor) -> torch.Tensor: + """ + obs: num_steps_per_env * num_envs // num_mini_batches, 313 + ///////////////////////////////////////// + 0: [0:3] (3,) 'base_lin_vel' + 1: [3:6] (3,) 'base_ang_vel' + 2: [6:9] (3,) 'proj_gravity' + 3: [9:13] (4,) 'concatenate_cmd' + 4: [13:14] (1,) 'time_left' + 5: [14:26] (12,) 'joint_pos' + 6: [26:38] (12,) 'joint_vel' + 7: [38:50] (12,) 'last_actions' + 8: [50:] (264,) 'height_scan' + //////////////////////////////////////// + + """ + B = obs.shape[0] + new_obs = obs.repeat(2, 1) + + # 0-2 base lin vel + new_obs[B : 2 * B, 1] = -new_obs[B : 2 * B, 1] + # 3-5 base ang vel + new_obs[B : 2 * B, 3] = -new_obs[B : 2 * B, 3] + new_obs[B : 2 * B, 5] = -new_obs[B : 2 * B, 5] + # 6-8 proj gravity + new_obs[B : 2 * B, 7] = -new_obs[B : 2 * B, 7] + # 9-13 cmd + new_obs[B : 2 * B, 10] = -new_obs[B : 2 * B, 10] + new_obs[B : 2 * B, 12] = -new_obs[B : 2 * B, 12] + + # X-symmetry: + # 00 = 'fl_hx' , 01 = fr_hx (-1) + # 01 = 'fr_hx' , 00 = fl_hx (-1) + # 02 = 'hl_hx' , 03 = hr_hx (-1) + # 03 = 'hr_hx' , 02 = hl_hx (-1) + # 04 = 'fl_hy' , 05 = fr_hy + # 05 = 'fr_hy' , 04 = fl_hy + # 06 = 'hl_hy' , 07 = hr_hy + # 07 = 'hr_hy' , 06 = hl_hy + # 08 = 'fl_kn' , 09 = fr_kn + # 09 = 'fr_kn' , 08 = fl_kn + # 10 = 'hl_kn' , 11 = hr_kn + # 11 = 'hr_kn' , 10 = hl_kn + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=obs.device) + new_obs[B : 2 * B, 14:26] = new_obs[B : 2 * B, 14:26][:, new_idx] * dir_change + new_obs[B : 2 * B, 26:38] = new_obs[B : 2 * B, 26:38][:, new_idx] * dir_change + new_obs[B : 2 * B, 38:50] = new_obs[B : 2 * B, 38:50][:, new_idx] * dir_change + new_obs[B : 2 * B, 50:] = new_obs[B : 2 * B, 50:].reshape(B, 11, 24).flip(1).flatten(1, 2) + + return new_obs + + +def aug_actions( + actions: torch.Tensor, actions_log_prob: torch.Tensor, action_mean: torch.Tensor, action_sigma: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + new_actions = actions.repeat(2, 1) + new_actions_log_prob = actions_log_prob.repeat(2, 1) + new_action_mean = action_mean.repeat(2, 1) + new_action_sigma = action_sigma.repeat(2, 1) + B = actions.shape[0] + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + new_action_mean[B : 2 * B, :] = new_action_mean[B : 2 * B, new_idx] * dir_change + new_action_sigma[B : 2 * B, :] = new_action_sigma[B : 2 * B, new_idx] + + return new_actions, new_actions_log_prob, new_action_mean, new_action_sigma + + +def aug_action(actions: torch.Tensor) -> torch.Tensor: + new_actions = actions.repeat(2, 1) + B = actions.shape[0] + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + + return new_actions + + +def aug_func(obs=None, actions=None, env=None): + aug_obs = None + aug_act = None + if obs is not None: + aug_obs = obs.repeat(2) + if "policy" in obs: + aug_obs["policy"] = aug_observation(obs["policy"]) + elif "critic" in obs: + aug_obs["critic"] = aug_observation(obs["critic"]) + else: + raise ValueError( + "nothing is augmented because not policy or critic keyword found in tensordict, you" + f" keys: {list(obs.keys())} \n please check for potential bug" + ) + if actions is not None: + aug_act = aug_action(actions) + return aug_obs, aug_act diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/spot_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/spot_env_cfg.py new file mode 100644 index 0000000..034dc42 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot/spot_env_cfg.py @@ -0,0 +1,135 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.utils import configclass + +import uwlab_assets.robots.spot as spot + +import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp + +from ... import balance_beams_env, stepping_beams_env, stepping_stones_env + + +@configclass +class SpotActionsCfg: + actions = spot.SPOT_JOINT_POSITION + + +@configclass +class SpotRewardsCfg(stepping_stones_env.RewardsCfg): + joint_torque_limits = RewTerm( + func=mdp.torque_limits, + weight=-0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*_h[xy]"), + "actuator_name": "spot_hip", + "ratio": 1.0, + }, + ) + + joint_torque_limits_knee = RewTerm( + func=mdp.torque_limits_knee, + weight=-0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*_kn"), + "ratio": 1.0, + }, + ) + + move_forward = RewTerm( + func=mdp.reward_forward_velocity, + weight=0.3, + params={ + "std": 1, + "max_iter": 200, + "init_amplification": 10, + "forward_vector": [1.0, 0.0, 0.0], + }, + ) + + foot_slip = RewTerm( + func=spot_mdp.foot_slip_penalty, + weight=-0.2, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*_foot"), + "sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*_foot"), + "threshold": 1.0, + }, + ) + + gait = RewTerm( + func=mdp.GaitReward, + weight=8.0, + params={ + "std": 0.1, + "max_err": 0.2, + "velocity_threshold": 0.5, + "synced_feet_pair_names": (("fl_foot", "hr_foot"), ("fr_foot", "hl_foot")), + "asset_cfg": SceneEntityCfg("robot"), + "sensor_cfg": SceneEntityCfg("contact_forces"), + "max_iterations": 400, + }, + ) + + joint_pos = RewTerm( + func=mdp.joint_position_penalty, + weight=-0.3, + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), + "stand_still_scale": 2.5, + "velocity_threshold": 0.5, + }, + ) + + def __post_init__(self): + self.undesired_contact.params["sensor_cfg"].body_names = [".*_uleg", ".*_lleg"] + self.contact_force_pen.params["sensor_cfg"].body_names = ".*_foot" + self.feet_accel.params["robot_cfg"].body_names = ".*_foot" + + self.base_accel.params["robot_cfg"].body_names = "body" + + +@configclass +class SpotEnvMixin: + actions: SpotActionsCfg = SpotActionsCfg() + rewards: SpotRewardsCfg = SpotRewardsCfg() + + def __post_init__(self: stepping_stones_env.SteppingStoneLocomotionEnvCfg): + # Ensure parent classes run their setup first + super().__post_init__() # type: ignore + + self.decimation = 10 + self.sim.dt = 0.002 + + # overwrite as spot's body names for sensors + self.scene.robot = spot.SPOT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/body" + self.scene.height_scanner.pattern_cfg.resolution = 0.15 + self.scene.height_scanner.pattern_cfg.size = (3.5, 1.5) + + # overwrite as spot's body names for events + self.events.add_base_mass.params["asset_cfg"].body_names = "body" + self.events.base_external_force_torque.params["asset_cfg"].body_names = "body" + + self.terminations.base_contact.params["sensor_cfg"].body_names = "body" + self.viewer.body_name = "body" + + +@configclass +class SteppingStoneSpotEnvCfg(SpotEnvMixin, stepping_stones_env.SteppingStoneLocomotionEnvCfg): + pass + + +@configclass +class BalanceBeamsSpotEnvCfg(SpotEnvMixin, balance_beams_env.BalanceBeamsLocomotionEnvCfg): + pass + + +@configclass +class SteppingBeamsSpotEnvCfg(SpotEnvMixin, stepping_beams_env.SteppingBeamsLocomotionEnvCfg): + pass diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/__init__.py new file mode 100644 index 0000000..ae1f005 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/__init__.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import gymnasium as gym + +from . import agents + +gym.register( + id="UW-Position-Stepping-Stone-Arm-Spot-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.spot_with_arm_env_cfg:SteppingStoneSpotEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsSpotPpoRunnerCfg", + }, +) + + +gym.register( + id="UW-Position-Balance-Beam-Arm-Spot-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.spot_with_arm_env_cfg:BalanceBeamsSpotEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsSpotPpoRunnerCfg", + }, +) + +gym.register( + id="UW-Position-Stepping-Beam-Arm-Spot-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.spot_with_arm_env_cfg:SteppingBeamsSpotEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:RiskyTerrainsSpotPpoRunnerCfg", + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/agents/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/agents/rsl_rl_cfg.py similarity index 61% rename from source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/agents/rsl_rl_cfg.py rename to source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/agents/rsl_rl_cfg.py index 3e7865f..425bb44 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/agents/rsl_rl_cfg.py @@ -1,37 +1,36 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg @configclass -class Base_PPORunnerCfg(RslRlOnPolicyRunnerCfg): +class RiskyTerrainsSpotPpoRunnerCfg(RslRlOnPolicyRunnerCfg): num_steps_per_env = 48 - max_iterations = 1500 - save_interval = 50 + max_iterations = 8000 + save_interval = 400 resume = False - experiment_name = "track_goal_agent" + experiment_name = "spot_risky_terrains" empirical_normalization = False policy = RslRlPpoActorCriticCfg( init_noise_std=1.0, - actor_hidden_dims=[256, 128, 64], - critic_hidden_dims=[256, 128, 64], + actor_hidden_dims=[512, 256, 128], + critic_hidden_dims=[512, 256, 128], activation="elu", ) algorithm = RslRlPpoAlgorithmCfg( value_loss_coef=1.0, use_clipped_value_loss=True, clip_param=0.2, - entropy_coef=0.006, + entropy_coef=0.005, num_learning_epochs=5, num_mini_batches=4, - learning_rate=1.0e-4, + learning_rate=1.0e-3, schedule="adaptive", - gamma=0.98, + gamma=0.99, lam=0.95, desired_kl=0.01, max_grad_norm=1.0, diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/augment.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/augment.py new file mode 100644 index 0000000..075034f --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/augment.py @@ -0,0 +1,108 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch + + +def aug_observation(obs: torch.Tensor) -> torch.Tensor: + """ + obs: num_steps_per_env * num_envs // num_mini_batches, 313 + ///////////////////////////////////////// + 0: [0:3] (3,) 'base_lin_vel' + 1: [3:6] (3,) 'base_ang_vel' + 2: [6:9] (3,) 'proj_gravity' + 3: [9:13] (4,) 'concatenate_cmd' + 4: [13:14] (1,) 'time_left' + 5: [14:26] (12,) 'joint_pos' + 6: [26:38] (12,) 'joint_vel' + 7: [38:50] (12,) 'last_actions' + 8: [50:] (264,) 'height_scan' + //////////////////////////////////////// + + """ + B = obs.shape[0] + new_obs = obs.repeat(2, 1) + + # 0-2 base lin vel + new_obs[B : 2 * B, 1] = -new_obs[B : 2 * B, 1] + # 3-5 base ang vel + new_obs[B : 2 * B, 3] = -new_obs[B : 2 * B, 3] + new_obs[B : 2 * B, 5] = -new_obs[B : 2 * B, 5] + # 6-8 proj gravity + new_obs[B : 2 * B, 7] = -new_obs[B : 2 * B, 7] + # 9-13 cmd + new_obs[B : 2 * B, 10] = -new_obs[B : 2 * B, 10] + new_obs[B : 2 * B, 12] = -new_obs[B : 2 * B, 12] + + # X-symmetry: + # 00 = 'fl_hx' , 01 = fr_hx (-1) + # 01 = 'fr_hx' , 00 = fl_hx (-1) + # 02 = 'hl_hx' , 03 = hr_hx (-1) + # 03 = 'hr_hx' , 02 = hl_hx (-1) + # 04 = 'fl_hy' , 05 = fr_hy + # 05 = 'fr_hy' , 04 = fl_hy + # 06 = 'hl_hy' , 07 = hr_hy + # 07 = 'hr_hy' , 06 = hl_hy + # 08 = 'fl_kn' , 09 = fr_kn + # 09 = 'fr_kn' , 08 = fl_kn + # 10 = 'hl_kn' , 11 = hr_kn + # 11 = 'hr_kn' , 10 = hl_kn + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=obs.device) + new_obs[B : 2 * B, 14:26] = new_obs[B : 2 * B, 14:26][:, new_idx] * dir_change + new_obs[B : 2 * B, 26:38] = new_obs[B : 2 * B, 26:38][:, new_idx] * dir_change + new_obs[B : 2 * B, 38:50] = new_obs[B : 2 * B, 38:50][:, new_idx] * dir_change + new_obs[B : 2 * B, 50:] = new_obs[B : 2 * B, 50:].reshape(B, 11, 24).flip(1).flatten(1, 2) + + return new_obs + + +def aug_actions( + actions: torch.Tensor, actions_log_prob: torch.Tensor, action_mean: torch.Tensor, action_sigma: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + new_actions = actions.repeat(2, 1) + new_actions_log_prob = actions_log_prob.repeat(2, 1) + new_action_mean = action_mean.repeat(2, 1) + new_action_sigma = action_sigma.repeat(2, 1) + B = actions.shape[0] + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + new_action_mean[B : 2 * B, :] = new_action_mean[B : 2 * B, new_idx] * dir_change + new_action_sigma[B : 2 * B, :] = new_action_sigma[B : 2 * B, new_idx] + + return new_actions, new_actions_log_prob, new_action_mean, new_action_sigma + + +def aug_action(actions: torch.Tensor) -> torch.Tensor: + new_actions = actions.repeat(2, 1) + B = actions.shape[0] + + new_idx = [1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10] + dir_change = torch.tensor([-1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1], device=actions.device) + new_actions[B : 2 * B, :] = new_actions[B : 2 * B, new_idx] * dir_change + + return new_actions + + +def aug_func(obs=None, actions=None, env=None): + aug_obs = None + aug_act = None + if obs is not None: + aug_obs = obs.repeat(2) + if "policy" in obs: + aug_obs["policy"] = aug_observation(obs["policy"]) + elif "critic" in obs: + aug_obs["critic"] = aug_observation(obs["critic"]) + else: + raise ValueError( + "nothing is augmented because not policy or critic keyword found in tensordict, you" + f" keys: {list(obs.keys())} \n please check for potential bug" + ) + if actions is not None: + aug_act = aug_action(actions) + return aug_obs, aug_act diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/spot_with_arm_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/spot_with_arm_env_cfg.py new file mode 100644 index 0000000..2b2bd2e --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/spot_with_arm/spot_with_arm_env_cfg.py @@ -0,0 +1,142 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.utils import configclass + +import uwlab_assets.robots.spot as spot + +import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp + +from ... import balance_beams_env, stepping_beams_env, stepping_stones_env + + +@configclass +class SpotActionsCfg: + actions = spot.SPOT_JOINT_POSITION + + +@configclass +class SpotRewardsCfg(stepping_stones_env.RewardsCfg): + joint_torque_limits = RewTerm( + func=mdp.torque_limits, + weight=-0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*_h[xy]"), + "actuator_name": "spot_hip", + "ratio": 1.0, + }, + ) + + joint_torque_limits_knee = RewTerm( + func=mdp.torque_limits_knee, + weight=-0.1, + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*_kn"), + "ratio": 1.0, + }, + ) + + move_forward = RewTerm( + func=mdp.reward_forward_velocity, + weight=0.3, + params={ + "std": 1, + "max_iter": 300, + "init_amplification": 10, + "forward_vector": [1.0, 0.0, 0.0], + }, + ) + + foot_slip = RewTerm( + func=spot_mdp.foot_slip_penalty, + weight=-0.2, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*_foot"), + "sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*_foot"), + "threshold": 1.0, + }, + ) + + foot_on_ground = RewTerm( + func=mdp.foot_on_ground, + weight=5.0, + params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*_foot"), "d": 0.5, "tr": 1.0}, + ) + + gait = RewTerm( + func=mdp.GaitReward, + weight=8.0, + params={ + "std": 0.1, + "max_err": 0.2, + "velocity_threshold": 0.5, + "synced_feet_pair_names": (("fl_foot", "hr_foot"), ("fr_foot", "hl_foot")), + "asset_cfg": SceneEntityCfg("robot"), + "sensor_cfg": SceneEntityCfg("contact_forces"), + "max_iterations": 400, + }, + ) + + joint_pos = RewTerm( + func=mdp.joint_position_penalty, + weight=-0.3, + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), + "stand_still_scale": 2.5, + "velocity_threshold": 0.5, + }, + ) + + def __post_init__(self): + self.undesired_contact.params["sensor_cfg"].body_names = [".*_uleg", ".*_lleg"] + self.contact_force_pen.params["sensor_cfg"].body_names = ".*_foot" + self.feet_accel.params["robot_cfg"].body_names = ".*_foot" + self.move_in_dir.params["max_iter"] = 200 + + self.base_accel.params["robot_cfg"].body_names = "body" + + +@configclass +class SpotEnvMixin: + actions: SpotActionsCfg = SpotActionsCfg() + rewards: SpotRewardsCfg = SpotRewardsCfg() + + def __post_init__(self: stepping_stones_env.SteppingStoneLocomotionEnvCfg): + # Ensure parent classes run their setup first + super().__post_init__() # type: ignore + + self.decimation = 10 + self.sim.dt = 0.002 + + # overwrite as spot's body names for sensors + self.scene.robot = spot.SPOT_WITH_ARM_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/body" + self.scene.height_scanner.pattern_cfg.resolution = 0.15 + self.scene.height_scanner.pattern_cfg.size = (3.5, 1.5) + + # overwrite as spot's body names for events + self.events.add_base_mass.params["asset_cfg"].body_names = "body" + self.events.base_external_force_torque.params["asset_cfg"].body_names = "body" + + self.terminations.base_contact.params["sensor_cfg"].body_names = "body" + self.viewer.body_name = "body" + + +@configclass +class SteppingStoneSpotEnvCfg(SpotEnvMixin, stepping_stones_env.SteppingStoneLocomotionEnvCfg): + pass + + +@configclass +class BalanceBeamsSpotEnvCfg(SpotEnvMixin, balance_beams_env.BalanceBeamsLocomotionEnvCfg): + pass + + +@configclass +class SteppingBeamsSpotEnvCfg(SpotEnvMixin, stepping_beams_env.SteppingBeamsLocomotionEnvCfg): + pass diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/terrains/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/terrains/__init__.py new file mode 100644 index 0000000..051e3c7 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/terrains/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from .terrain_cfg import * diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/terrains/terrain_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/terrains/terrain_cfg.py new file mode 100644 index 0000000..ba6077a --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/config/terrains/terrain_cfg.py @@ -0,0 +1,101 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import isaaclab.terrains.terrain_generator as terrain_generator +from isaaclab.terrains.terrain_generator_cfg import TerrainGeneratorCfg + +from uwlab.terrains.trimesh import ( + MeshBalanceBeamsTerrainCfg, + MeshSteppingBeamsTerrainCfg, + MeshStonesEverywhereTerrainCfg, +) +from uwlab.terrains.utils import FlatPatchSamplingCfg, PatchSamplingCfg + + +def patched_find_flat_patches(*args, **kwargs) -> None: + patch_key = "patch_radius" + kwargs[patch_key]["patched"] = True + cfg_class = kwargs[patch_key]["cfg"] + cfg_class_args = {key: val for key, val in kwargs[patch_key].items() if key not in ["func", "cfg"]} + patch_sampling_cfg: PatchSamplingCfg = cfg_class(**cfg_class_args) + return patch_sampling_cfg.func(kwargs["wp_mesh"], kwargs["origin"], patch_sampling_cfg) + + +terrain_generator.find_flat_patches = patched_find_flat_patches + + +RISKY_TERRAINS_CFG = TerrainGeneratorCfg( + size=(10, 10), + border_width=10, + num_rows=10, + num_cols=40, + horizontal_scale=0.1, + vertical_scale=0.005, + slope_threshold=0.75, + use_cache=False, + curriculum=True, + sub_terrains={ + "stones_everywhere": MeshStonesEverywhereTerrainCfg( + w_gap=(0.08, 0.26), + w_stone=(0.92, 0.2), + s_max=(0.036, 0.118), + h_max=(0.01, 0.1), + holes_depth=-10.0, + platform_width=1.5, + ) + }, +) + +BALANCE_BEAMS_CFG = TerrainGeneratorCfg( + size=(10, 10), + border_width=10, + num_rows=10, + num_cols=20, + horizontal_scale=0.1, + vertical_scale=0.005, + slope_threshold=0.75, + use_cache=False, + curriculum=True, + sub_terrains={ + "balance_beams": MeshBalanceBeamsTerrainCfg( + platform_width=2.0, + h_offset=(0.01, 0.1), + w_stone=(0.25, 0.25), + mid_gap=0.25, + flat_patch_sampling={ + "target": FlatPatchSamplingCfg( + patch_radius=0.4, + num_patches=10, + x_range=(4, 6), + y_range=(-1, 1), + z_range=(-0.05, 0.05), + max_height_diff=0.05, + ) + }, + ), + }, +) + +STEPPING_BEAMS_CFG = TerrainGeneratorCfg( + size=(10, 10), + border_width=10, + num_rows=10, + num_cols=20, + horizontal_scale=0.1, + vertical_scale=0.005, + slope_threshold=0.75, + use_cache=False, + curriculum=True, + sub_terrains={ + "stepping_beams": MeshSteppingBeamsTerrainCfg( + platform_width=2.0, + h_offset=(0.01, 0.1), + w_stone=(0.5, 0.2), + l_stone=(0.8, 1.6), + gap=(0.15, 0.5), + yaw=(0, 15), + ) + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/__init__.py new file mode 100644 index 0000000..6b6c288 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.envs.mdp import * + +from uwlab.envs.mdp import * # noqa: F401, F403 + +from .commands_cfg import * # noqa: F401, F403 +from .curriculums import * # noqa: F401, F403 +from .events 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/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/commands.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/commands.py new file mode 100644 index 0000000..8e73045 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/commands.py @@ -0,0 +1,160 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from isaaclab.assets import Articulation +from isaaclab.managers import CommandTerm +from isaaclab.markers import VisualizationMarkers +from isaaclab.utils.math import quat_apply_inverse, quat_from_euler_xyz, wrap_to_pi, yaw_quat + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + + from .commands_cfg import UniformPolarPose2dCommandCfg + + +class UniformPolarPose2dCommand(CommandTerm): + cfg: UniformPolarPose2dCommandCfg + """Configuration for the command generator.""" + + def __init__(self, cfg: UniformPolarPose2dCommandCfg, env: ManagerBasedEnv): + """Initialize the command generator class. + + Args: + cfg: The configuration parameters for the command generator. + env: The environment object. + """ + # initialize the base class + super().__init__(cfg, env) + + # obtain the robot and terrain assets + # -- robot + self.robot: Articulation = env.scene[cfg.asset_name] + + # crete buffers to store the command + # -- commands: (x, y, z, heading) + self.pos_command_w = torch.zeros(self.num_envs, 3, device=self.device) + self.heading_command_w = torch.zeros(self.num_envs, device=self.device) + self.pos_command_b = torch.zeros_like(self.pos_command_w) + self.heading_command_b = torch.zeros_like(self.heading_command_w) + # -- metrics + self.metrics["error_pos_2d"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_heading"] = torch.zeros(self.num_envs, device=self.device) + # + self.total_distance = torch.zeros(self.num_envs, device=self.device) + + def __str__(self) -> str: + msg = "PositionCommand:\n" + msg += f"\tCommand dimension: {tuple(self.command.shape[1:])}\n" + msg += f"\tResampling time range: {self.cfg.resampling_time_range}" + return msg + + """ + Properties + """ + + @property + def command(self) -> torch.Tensor: + """ + The desired 2D-pose in the base frame, consisting of x, y position and heading angle. + + Shape is (num_envs, 3), where: + - command[:, 0] is the desired x-position in the base frame + - command[:, 1] is the desired y-position in the base frame + - command[:, 2] is the desired z-position in the base frame + - command[:, 3] is the desired heading angle (in radians) + + Note: The z component of the position is ignored. + """ + # ignore the z component + return torch.cat([self.pos_command_b[:, :3], self.heading_command_b.unsqueeze(1)], dim=1) + + """ + Implementation specific functions. + """ + + def _update_metrics(self): + # logs data + self.metrics["error_pos_2d"] = torch.norm(self.pos_command_w[:, :2] - self.robot.data.root_pos_w[:, :2], dim=1) + self.metrics["error_heading"] = torch.abs(wrap_to_pi(self.heading_command_w - self.robot.data.heading_w)) + + def _resample_command(self, env_ids: Sequence[int]): + # obtain env origins for the environments + self.pos_command_w[env_ids] = self.robot.data.root_state_w[env_ids, :3] + # offset the position command by the current root position + r = ( + torch.rand(len(env_ids), device=self.device) + * (self.cfg.ranges.distance_range[1] - self.cfg.ranges.distance_range[0]) + + self.cfg.ranges.distance_range[0] + ) + angle = torch.rand(len(env_ids), device=self.device) * 2 * torch.pi + self.total_distance[env_ids] = r + # set the position command x and y values + self.pos_command_w[env_ids, 0] += r * torch.cos(angle) + self.pos_command_w[env_ids, 1] += r * torch.sin(angle) + + if self.cfg.simple_heading: + # set heading command to point towards target + target_vec = self.pos_command_w[env_ids] - self.robot.data.root_pos_w[env_ids] + target_direction = torch.atan2(target_vec[:, 1], target_vec[:, 0]) + flipped_target_direction = wrap_to_pi(target_direction + torch.pi) + + # compute errors to find the closest direction to the current heading + # this is done to avoid the discontinuity at the -pi/pi boundary + curr_to_target = wrap_to_pi(target_direction - self.robot.data.heading_w[env_ids]).abs() + curr_to_flipped_target = wrap_to_pi(flipped_target_direction - self.robot.data.heading_w[env_ids]).abs() + + # set the heading command to the closest direction + self.heading_command_w[env_ids] = torch.where( + curr_to_target < curr_to_flipped_target, + target_direction, + flipped_target_direction, + ) + else: + # random heading command + r = torch.empty(len(env_ids), device=self.device) + self.heading_command_w[env_ids] = r.uniform_(*self.cfg.ranges.heading) + + def _update_command(self): + """Re-target the position command to the current root state.""" + target_vec = self.pos_command_w - self.robot.data.root_pos_w[:, :3] + self.pos_command_b[:] = quat_apply_inverse(yaw_quat(self.robot.data.root_quat_w), target_vec) + self.heading_command_b[:] = wrap_to_pi(self.heading_command_w - self.robot.data.heading_w) + + def _set_debug_vis_impl(self, debug_vis: bool): + # create markers if necessary for the first tome + if debug_vis: + if not hasattr(self, "goal_pose_visualizer"): + self.goal_pose_visualizer = VisualizationMarkers(self.cfg.goal_pose_visualizer_cfg) + if not hasattr(self, "current_pose_visualizer"): + self.current_pose_visualizer = VisualizationMarkers(self.cfg.current_pose_visualizer_cfg) + # set their visibility to true + self.goal_pose_visualizer.set_visibility(True) + self.current_pose_visualizer.set_visibility(True) + else: + if hasattr(self, "goal_pose_visualizer"): + self.goal_pose_visualizer.set_visibility(False) + if hasattr(self, "current_pose_visualizer"): + self.current_pose_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # update the box marker + self.goal_pose_visualizer.visualize( + translations=self.pos_command_w, + orientations=quat_from_euler_xyz( + torch.zeros_like(self.heading_command_w), + torch.zeros_like(self.heading_command_w), + self.heading_command_w, + ), + ) + self.current_pose_visualizer.visualize( + translations=self.robot.data.root_pos_w, + orientations=self.robot.data.root_quat_w, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/commands_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/commands_cfg.py new file mode 100644 index 0000000..92c37a5 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/commands_cfg.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from isaaclab.managers import CommandTermCfg +from isaaclab.markers import VisualizationMarkersCfg +from isaaclab.markers.config import BLUE_ARROW_X_MARKER_CFG, GREEN_ARROW_X_MARKER_CFG +from isaaclab.utils import configclass + +from .commands import UniformPolarPose2dCommand + + +@configclass +class UniformPolarPose2dCommandCfg(CommandTermCfg): + class_type: type = UniformPolarPose2dCommand + + asset_name: str = MISSING + + simple_heading: bool = MISSING + + @configclass + class Ranges: + distance_range: tuple[float, float] = MISSING + heading: tuple[float, float] = MISSING + + ranges: Ranges = MISSING + + goal_pose_visualizer_cfg: VisualizationMarkersCfg = GREEN_ARROW_X_MARKER_CFG.replace( + prim_path="/Visuals/Command/pose_goal" + ) + current_pose_visualizer_cfg: VisualizationMarkersCfg = BLUE_ARROW_X_MARKER_CFG.replace( + prim_path="Visuals/Command/current_pose" + ) + + goal_pose_visualizer_cfg.markers["arrow"].scale = (0.2, 0.2, 0.2) + current_pose_visualizer_cfg.markers["arrow"].scale = (0.2, 0.2, 0.2) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/curriculums.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/curriculums.py new file mode 100644 index 0000000..c676e34 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/curriculums.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from isaaclab.terrains import TerrainImporter + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def terrain_levels_risky( + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + demotion_fraction: float = 0.1, +): + terrain: TerrainImporter = env.scene.terrain + command = env.command_manager.get_command("target_cmd") + command_term = env.command_manager.get_term("target_cmd") + + goal_position_b = command[env_ids, :2] + + total_distance = (command_term.pos_command_w[env_ids, :2] - env.scene.env_origins[env_ids, :2]).norm(2, dim=1) + + distance_to_goal = goal_position_b.norm(2, dim=1) + move_up = distance_to_goal < 0.4 + move_down = (distance_to_goal / total_distance) > (1 - demotion_fraction) + terrain.update_env_origins(env_ids, move_up, move_down) + return torch.mean(terrain.terrain_levels.float()) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/events.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/events.py new file mode 100644 index 0000000..9ec1ed8 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/events.py @@ -0,0 +1,32 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def reset_episode_length_s( + env: ManagerBasedRLEnv, envs_id: torch.Tensor, episode_length_s: tuple[float, float] = (5.0, 7.0) +): + if "max_episode_length" not in env.extensions.keys(): + env.extensions["max_episode_length_s"] = ( + torch.rand(env.num_envs, device=env.device) * (episode_length_s[1] - episode_length_s[0]) + + episode_length_s[0] + ) + env.extensions["max_episode_length"] = (env.extensions["max_episode_length_s"] / env.step_dt).to(torch.int16) + + else: + env.extensions["max_episode_length_s"][envs_id] = ( + torch.rand(envs_id.shape[0], device=env.device) * (episode_length_s[1] - episode_length_s[0]) + + episode_length_s[0] + ) + env.extensions["max_episode_length"][envs_id] = ( + env.extensions["max_episode_length_s"][envs_id] / env.step_dt + ).to(torch.int16) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/observations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/observations.py new file mode 100644 index 0000000..7397276 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/observations.py @@ -0,0 +1,33 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch + +from isaaclab.envs import ManagerBasedRLEnv + + +def time_left(env: ManagerBasedRLEnv) -> torch.Tensor: + if not hasattr(env, "extensions"): + setattr(env, "extensions", {}) + if "max_episode_length_s" in env.extensions: + if hasattr(env, "episode_length_buf"): + life_left = (1 - (env.episode_length_buf.float() / env.extensions["max_episode_length"])) * env.extensions[ + "max_episode_length_s" + ] + else: + life_left = ( + torch.ones(env.num_envs, device=env.device, dtype=torch.float) * env.extensions["max_episode_length_s"] + ) + else: + life_left = torch.ones(env.num_envs, device=env.device, dtype=torch.float) * env.max_episode_length_s + return life_left.view(-1, 1) + + +def generated_modified_commands(env: ManagerBasedRLEnv, command_name: str) -> torch.Tensor: + """The generated command from command term in the command manager with the given name.""" + return torch.cat( + (env.command_manager.get_command(command_name)[:, :2], env.command_manager.get_command(command_name)[:, 3:4]), + dim=-1, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/rewards.py new file mode 100644 index 0000000..9fc3aad --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/rewards.py @@ -0,0 +1,442 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch + +from isaaclab.assets import Articulation, RigidObject +from isaaclab.envs import ManagerBasedRLEnv +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers.manager_base import ManagerTermBase +from isaaclab.managers.manager_term_cfg import RewardTermCfg +from isaaclab.sensors import ContactSensor + + +def joint_vel_limit_pen( + env: ManagerBasedRLEnv, robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), limits_factor: float = 0.9 +): + robot: Articulation = env.scene[robot_cfg.name] + joint_vel = robot.data.joint_vel[:, robot_cfg.joint_ids] + joint_vel_limit = robot.data.soft_joint_vel_limits[:, robot_cfg.joint_ids] + return torch.sum((joint_vel.abs() - limits_factor * joint_vel_limit).clamp_min_(0), dim=-1) + + +def base_accel_pen( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot", body_names="base"), + ratio: float = 0.02, +): + robot: Articulation = env.scene[robot_cfg.name] + base_angle_accel = robot.data.body_ang_acc_w[:, robot_cfg.body_ids].norm(2, dim=-1).pow(2) + base_lin_accel = robot.data.body_lin_acc_w[:, robot_cfg.body_ids].norm(2, dim=-1).pow(2) + return (base_lin_accel + ratio * base_angle_accel).squeeze(-1) + + +def feet_accel_l1_pen( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot", body_names=".*FOOT"), +): + robot: Articulation = env.scene[robot_cfg.name] + feet_acc = robot.data.body_lin_acc_w[:, robot_cfg.body_ids].norm(2, dim=-1) + return torch.sum(feet_acc, dim=-1) + + +def contact_forces_pen( + env: ManagerBasedRLEnv, threshold: float = 700, sensor_cfg: SceneEntityCfg = SceneEntityCfg("contact_sensor") +): + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + net_contact_forces = contact_sensor.data.net_forces_w_history + force = torch.norm(net_contact_forces[:, 0, sensor_cfg.body_ids], dim=-1) + return torch.clamp(force - threshold, 0, threshold).pow(2).sum(-1) + + +def position_tracking(env: ManagerBasedRLEnv, tr: float = 2.0): + desired_xy = env.command_manager.get_command("target_cmd")[:, :2] + start_step = env.extensions["max_episode_length"] * (1 - tr / env.extensions["max_episode_length_s"]) + r_pos_tracking = 1 / tr * (1 / (1 + desired_xy.norm(2, -1).pow(2))) * (env.episode_length_buf > start_step).float() + return r_pos_tracking + + +def heading_tracking(env: ManagerBasedRLEnv, d: float = 2.0, tr: float = 4.0): + desired_heading = env.command_manager.get_command("target_cmd")[:, 3] + start_step = env.extensions["max_episode_length"] * (1 - tr / env.extensions["max_episode_length_s"]) + current_dist = env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1) + r_heading_tracking = ( + 1 + / tr + * (1 / (1 + desired_heading.pow(2))) + * (current_dist < d).float() + * (env.episode_length_buf > start_step).float() + ) + return r_heading_tracking + + +def dont_wait( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + d: float = 1.0, + velocity_threshold: float = 0.2, +): + robot: Articulation = env.scene[robot_cfg.name] + dist_to_goal = env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1) + return (dist_to_goal > d).float() * (robot.data.root_lin_vel_w.norm(2, -1) < velocity_threshold).float() + + +def move_in_dir( + env: ManagerBasedRLEnv, + max_iter: int = 150, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + robot: Articulation = env.scene[robot_cfg.name] + lin_vel_b = robot.data.root_lin_vel_b[:, :2] + target_dir = env.command_manager.get_command("target_cmd")[:, :2] + current_iter = int(env.common_step_counter / 48) + return torch.cosine_similarity(lin_vel_b, target_dir, dim=-1) * float(current_iter < max_iter) + + +def foot_on_ground(env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg, d: float = 0.25, tr: float = 1.0): + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + foot_on_ground_rew = 1 - torch.tanh(contact_sensor.data.current_air_time[:, sensor_cfg.body_ids].sum(-1) / 0.5) + + distance_succ_mask = (env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1)) < d + rew_window_scaler = ( + 1 + / tr + * ( + env.episode_length_buf + > ((1 - tr / env.extensions["max_episode_length_s"]) * env.extensions["max_episode_length"]) + ).float() + ) + + return torch.where(distance_succ_mask, foot_on_ground_rew * rew_window_scaler, 0.0) + + +def stand_still( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + d: float = 0.25, + phi: float = 0.5, + tr: float = 1.0, +): + robot: Articulation = env.scene[robot_cfg.name] + movement_penalty = 2.5 * robot.data.root_lin_vel_w.norm(2, -1) + 1.0 * robot.data.root_ang_vel_w.norm(2, -1) + + heading_succ_mask = (env.command_manager.get_command("target_cmd")[:, 3].abs()) < phi + distance_succ_mask = (env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1)) < d + rew_window_scaler = ( + 1 + / tr + * ( + env.episode_length_buf + > ((1 - tr / env.extensions["max_episode_length_s"]) * env.extensions["max_episode_length"]) + ).float() + ) + + return torch.where(heading_succ_mask & distance_succ_mask, movement_penalty * rew_window_scaler, 0.0) + + +def illegal_contact_penalty(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg): + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + net_contact_forces = contact_sensor.data.net_forces_w_history + # check if any contact force exceeds the threshold + return torch.any( + torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] > threshold, dim=1 + ).float() + + +import os +import torch.nn.functional as F +from torch import nn + + +class RNDCuriosityNet(nn.Module): + def __init__(self, in_dim, out_dim, n_hid): + super().__init__() + self.in_dim = in_dim + self.out_dim = out_dim + self.n_hid = n_hid + + self.fc1 = nn.Linear(in_dim, n_hid) + self.fc2 = nn.Linear(n_hid, n_hid) + self.fc3 = nn.Linear(n_hid, out_dim) + + def forward(self, x): + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + y = self.fc3(x) + return y + + +def init_weights(m): + if isinstance(m, nn.Linear): + nn.init.xavier_uniform_(m.weight) + + +class CuriosityReward(ManagerTermBase): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + # initialize the base class + super().__init__(cfg, env) + self.obs_dim: int = env.observation_manager.group_obs_dim["policy"][0] + self.action_dim = env.action_manager.action_term_dim[0] + self.M1 = RNDCuriosityNet(self.obs_dim + self.action_dim, 1, 128).to(env.device) # prediction network + self.M2 = RNDCuriosityNet(self.obs_dim + self.action_dim, 1, 256).to(env.device) # frozen target network + self.M1.apply(init_weights) + self.M2.apply(init_weights) # init weights of target network + self.optimizer = torch.optim.Adam(self.M1.parameters(), lr=cfg.params.get("lr")) + self.loss = nn.L1Loss(reduce=False) + + def __call__(self, env: ManagerBasedRLEnv, optimization_weight: float, lr: float) -> torch.Tensor: + if "log_dir" in env.extensions: + if env.common_step_counter == 1: + if not os.path.exists(os.path.join(env.extensions["log_dir"], "RND")): + os.mkdir(os.path.join(env.extensions["log_dir"], "RND")) + torch.save(self.M2.state_dict(), os.path.join(env.extensions["log_dir"], "RND", "M2.pth")) + + if (env.common_step_counter // 48) % 500 == 0: + torch.save(self.M1.state_dict(), os.path.join(env.extensions["log_dir"], "RND", "M1.pth")) + + with torch.inference_mode(False): + obs: torch.Tensor = ( + env.obs_buf["policy"] if hasattr(env, "obs_buf") else env.observation_manager.compute()["policy"] + ) + action = env.action_manager.action + predict_value = self.M1(torch.cat([obs.detach(), action.detach()], dim=-1)) + with torch.no_grad(): + target_value = self.M2(torch.cat([obs, action], dim=-1)) + reward = self.loss(predict_value, target_value) + loss = (reward * optimization_weight).mean() + self.optimizer.zero_grad() + loss.backward() + self.optimizer.step() + return reward.squeeze(-1).detach().clamp_(-10, 10) + + +# balance beams + + +def aggressive_motion( + env: ManagerBasedRLEnv, + threshold: float = 1.0, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + robot: Articulation = env.scene[robot_cfg.name] + horizontal_velocity = robot.data.root_lin_vel_w[:, :2].norm(2, -1) + return (horizontal_velocity - threshold).pow(2) * (horizontal_velocity > threshold).float() + + +def stand_pos( + env: ManagerBasedRLEnv, + base_height: float = 0.6, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + sensor_cfg: SceneEntityCfg = SceneEntityCfg("contact_forces", body_names=".*FOOT"), + tr: float = 1, + phi: float = 0.5, + d: float = 0.25, +): + robot: Articulation = env.scene[robot_cfg.name] + return ( + ((robot.data.root_pos_w[:, -1] - base_height).abs() + robot.data.projected_gravity_b[:, :2].pow(2).sum(-1)) + * ((env.command_manager.get_command("target_cmd")[:, :2].norm(2, -1)) < d).float() + * ( + 1 + / tr + * ( + env.episode_length_buf + > ((1 - tr / env.extensions["max_episode_length_s"]) * env.extensions["max_episode_length"]) + ).float() + ) + * ((env.command_manager.get_command("target_cmd")[:, 3].abs()) < phi).float() + ) + + +def torque_limits( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + actuator_name: str = "legs", + ratio: float = 1.0, +) -> torch.Tensor: + asset: Articulation = env.scene[asset_cfg.name] + computed_torque = asset.data.computed_torque[:, asset_cfg.joint_ids].abs() # shape: [batch, joint] + limits = ratio * asset.actuators.get(actuator_name).effort_limit + out_of_limits = torch.clamp(computed_torque - limits, min=0) + return torch.sum(out_of_limits, dim=1) + + +# ============ Spot tailored rewards ============ + + +def torque_limits_knee( + env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ratio: float = 1.0 +) -> torch.Tensor: + asset: Articulation = env.scene[asset_cfg.name] + computed_torque = asset.data.computed_torque[:, asset_cfg.joint_ids] # shape: [batch, joint] + applied_torque = asset.data.applied_torque[:, asset_cfg.joint_ids] + out_of_limits = ratio * (computed_torque - applied_torque).abs() + return torch.sum(out_of_limits, dim=1) + + +def reward_forward_velocity( + env: ManagerBasedRLEnv, + std: float, + forward_vector, + max_iter: int = 150, + init_amplification: float = 1.0, + distance_threshold: float = 0.4, + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +) -> torch.Tensor: + """Reward tracking of linear velocity commands (xy axes) using exponential kernel.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + root_lin_vel_b = asset.data.root_lin_vel_b + forward_velocity = root_lin_vel_b * torch.tensor(forward_vector, device=env.device, dtype=root_lin_vel_b.dtype) + forward_reward = torch.sum(forward_velocity, dim=1) + current_iter = int(env.common_step_counter / 48) + coeff = init_amplification if current_iter < max_iter else 1.0 + distance = torch.norm(env.command_manager.get_command("target_cmd")[:, :2], dim=1) + return torch.where(distance > distance_threshold, torch.tanh(forward_reward / std) * coeff, 0.0) + + +def air_time_reward( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg, + sensor_cfg: SceneEntityCfg, + mode_time: float, + velocity_threshold: float, +) -> torch.Tensor: + """Reward longer feet air and contact time.""" + # extract the used quantities (to enable type-hinting) + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + asset: Articulation = env.scene[asset_cfg.name] + if contact_sensor.cfg.track_air_time is False: + raise RuntimeError("Activate ContactSensor's track_air_time!") + # compute the reward + current_air_time = contact_sensor.data.current_air_time[:, sensor_cfg.body_ids] + current_contact_time = contact_sensor.data.current_contact_time[:, sensor_cfg.body_ids] + + t_max = torch.max(current_air_time, current_contact_time) + t_min = torch.clip(t_max, max=mode_time) + stance_cmd_reward = torch.clip(current_contact_time - current_air_time, -mode_time, mode_time) + # cmd = torch.norm(env.command_manager.get_command("base_velocity"), dim=1).unsqueeze(dim=1).expand(-1, 4) + body_vel = torch.linalg.norm(asset.data.root_com_lin_vel_b[:, :2], dim=1).unsqueeze(dim=1).expand(-1, 4) + distance = torch.norm(env.command_manager.get_command("target_cmd")[:, :2], dim=1) + reward = torch.where( + (distance > 0.4) & (body_vel > velocity_threshold), + torch.where(t_max < mode_time, t_min, 0), + stance_cmd_reward, + ) + return torch.sum(reward, dim=1) + + +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. + """ + + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + """Initialize the term. + + Args: + cfg: The configuration of the reward. + env: The RL environment instance. + """ + super().__init__(cfg, env) + self.std: float = cfg.params["std"] + self.max_err: float = cfg.params["max_err"] + self.velocity_threshold: float = cfg.params["velocity_threshold"] + self.contact_sensor: ContactSensor = env.scene.sensors[cfg.params["sensor_cfg"].name] + self.asset: Articulation = env.scene[cfg.params["asset_cfg"].name] + # match foot body names with corresponding foot body ids + synced_feet_pair_names = cfg.params["synced_feet_pair_names"] + if ( + len(synced_feet_pair_names) != 2 + or len(synced_feet_pair_names[0]) != 2 + or len(synced_feet_pair_names[1]) != 2 + ): + raise ValueError("This reward only supports gaits with two pairs of synchronized feet, like trotting.") + synced_feet_pair_0 = self.contact_sensor.find_bodies(synced_feet_pair_names[0])[0] + synced_feet_pair_1 = self.contact_sensor.find_bodies(synced_feet_pair_names[1])[0] + self.synced_feet_pairs = [synced_feet_pair_0, synced_feet_pair_1] + + def __call__( + self, + env: ManagerBasedRLEnv, + std: float, + max_err: float, + velocity_threshold: float, + synced_feet_pair_names, + asset_cfg: SceneEntityCfg, + sensor_cfg: SceneEntityCfg, + max_iterations: int = 500, + ) -> torch.Tensor: + """Compute the reward. + + This reward is defined as a multiplication between six terms where two of them enforce pair feet + being in sync and the other four rewards if all the other remaining pairs are out of sync + + Args: + env: The RL environment instance. + Returns: + The reward value. + """ + current_iter = int(env.common_step_counter / 48) + if current_iter > max_iterations: + return torch.zeros(self.num_envs, device=self.device) + # for synchronous feet, the contact (air) times of two feet should match + sync_reward_0 = self._sync_reward_func(self.synced_feet_pairs[0][0], self.synced_feet_pairs[0][1]) + sync_reward_1 = self._sync_reward_func(self.synced_feet_pairs[1][0], self.synced_feet_pairs[1][1]) + sync_reward = sync_reward_0 * sync_reward_1 + # for asynchronous feet, the contact time of one foot should match the air time of the other one + async_reward_0 = self._async_reward_func(self.synced_feet_pairs[0][0], self.synced_feet_pairs[1][0]) + async_reward_1 = self._async_reward_func(self.synced_feet_pairs[0][1], self.synced_feet_pairs[1][1]) + async_reward_2 = self._async_reward_func(self.synced_feet_pairs[0][0], self.synced_feet_pairs[1][1]) + async_reward_3 = self._async_reward_func(self.synced_feet_pairs[1][0], self.synced_feet_pairs[0][1]) + async_reward = async_reward_0 * async_reward_1 * async_reward_2 * async_reward_3 + # only enforce gait if cmd > 0 + # cmd = torch.norm(env.command_manager.get_command("base_velocity"), dim=1) + body_vel = torch.linalg.norm(self.asset.data.root_com_lin_vel_b[:, :2], dim=1) + distance = torch.norm(env.command_manager.get_command("target_cmd")[:, :2], dim=1) + approach_gait_rew = torch.where( + ((distance > 0.4) & (body_vel > self.velocity_threshold)), sync_reward * async_reward, 0.0 + ) + stance_rew = torch.where((distance < 0.4), 1 - torch.tanh(body_vel / 0.1), 0.0) + return approach_gait_rew + stance_rew + + """ + Helper functions. + """ + + def _sync_reward_func(self, foot_0: int, foot_1: int) -> torch.Tensor: + """Reward synchronization of two feet.""" + air_time = self.contact_sensor.data.current_air_time + contact_time = self.contact_sensor.data.current_contact_time + # penalize the difference between the most recent air time and contact time of synced feet pairs. + se_air = torch.clip(torch.square(air_time[:, foot_0] - air_time[:, foot_1]), max=self.max_err**2) + se_contact = torch.clip(torch.square(contact_time[:, foot_0] - contact_time[:, foot_1]), max=self.max_err**2) + return torch.exp(-(se_air + se_contact) / self.std) + + def _async_reward_func(self, foot_0: int, foot_1: int) -> torch.Tensor: + """Reward anti-synchronization of two feet.""" + air_time = self.contact_sensor.data.current_air_time + contact_time = self.contact_sensor.data.current_contact_time + # penalize the difference between opposing contact modes air time of feet 1 to contact time of feet 2 + # and contact time of feet 1 to air time of feet 2) of feet pairs that are not in sync with each other. + se_act_0 = torch.clip(torch.square(air_time[:, foot_0] - contact_time[:, foot_1]), max=self.max_err**2) + se_act_1 = torch.clip(torch.square(contact_time[:, foot_0] - air_time[:, foot_1]), max=self.max_err**2) + return torch.exp(-(se_act_0 + se_act_1) / self.std) + + +def joint_position_penalty( + env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, stand_still_scale: float, velocity_threshold: float +) -> torch.Tensor: + """Penalize joint position error from default on the articulation.""" + asset: Articulation = env.scene[asset_cfg.name] + distance = torch.norm(env.command_manager.get_command("target_cmd")[:, :2], dim=1) + body_vel = torch.linalg.norm(asset.data.root_lin_vel_b[:, :2], dim=1) + reward = torch.linalg.norm((asset.data.joint_pos - asset.data.default_joint_pos), dim=1) + return torch.where( + torch.logical_or(distance > 0.4, body_vel > velocity_threshold), reward, stand_still_scale * reward + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/terminations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/terminations.py new file mode 100644 index 0000000..3b2fa17 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/mdp/terminations.py @@ -0,0 +1,18 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def custom_time_out(env: ManagerBasedRLEnv) -> torch.Tensor: + """Terminate the episode when the episode length exceeds the maximum episode length.""" + assert "max_episode_length" in env.extensions, "The maximum episode length is not set." + return env.episode_length_buf >= env.extensions["max_episode_length"] diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_beams_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_beams_env.py new file mode 100644 index 0000000..c523fe6 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_beams_env.py @@ -0,0 +1,110 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.managers import CurriculumTermCfg as CurrTerm +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.utils import configclass +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp + +from . import stepping_stones_env +from .config.terrains.terrain_cfg import STEPPING_BEAMS_CFG + + +@configclass +class SteppingBeamSceneCfg(stepping_stones_env.SteppingStoneSceneCfg): + def __post_init__(self): + self.terrain.terrain_generator = STEPPING_BEAMS_CFG + + +@configclass +class CommandsCfg: + """Commands for the MDP.""" + + target_cmd = mdp.UniformPose2dCommandCfg( + asset_name="robot", + resampling_time_range=(10, 10), + debug_vis=True, + simple_heading=False, + ranges=mdp.UniformPose2dCommandCfg.Ranges(pos_x=(2.5, 4.5), pos_y=(-0.1, 0.1), heading=(-0.785, 0.785)), + ) + + +@configclass +class ObservationsCfg: + """Observations for the MDP.""" + + @configclass + class PolicyCfg(stepping_stones_env.ObservationsCfg.PolicyCfg): + def __post_init__(self): + super().__post_init__() + self.height_scan.noise = Unoise(n_min=-0.025, n_max=0.025) + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventsCfg(stepping_stones_env.EventsCfg): + """Events for the MDP.""" + + reset_base = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": {"x": (-0.25, 0.25), "y": (-0.25, 0.25), "yaw": (-3.14, 3.14)}, + "velocity_range": { + "x": (-0.5, 0.5), + "y": (-0.5, 0.5), + "z": (-0.5, 0.5), + "roll": (-0.5, 0.5), + "pitch": (-0.5, 0.5), + "yaw": (-0.5, 0.5), + }, + }, + ) + + +@configclass +class RewardsCfg(stepping_stones_env.RewardsCfg): + """Rewards for the MDP.""" + + # Balance Beam specific rewards + + aggressive_motion = RewTerm(func=mdp.aggressive_motion, weight=-5.0, params={"threshold": 1.0}) + + stand_pose = RewTerm(func=mdp.stand_pos, weight=-5.0, params={"base_height": 0.6, "tr": 1.0, "phi": 0.5, "d": 0.25}) + + def __post_init__(self): + self.position_tracking.weight = 25.0 + self.head_tracking.weight = 12.0 + self.joint_torque_limits.weight = -0.5 + self.stand_still.params["d"] = 0.25 + + +@configclass +class CurriculumCfg: + """Curriculum for the MDP.""" + + terrain_levels = CurrTerm( + func=mdp.terrain_levels_risky, # type: ignore + params={ + "demotion_fraction": 0.00, + }, + ) + + +@configclass +class SteppingBeamsLocomotionEnvCfg(stepping_stones_env.SteppingStoneLocomotionEnvCfg): + scene: SteppingBeamSceneCfg = SteppingBeamSceneCfg(num_envs=4096, env_spacing=2.5) + observations: ObservationsCfg = ObservationsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + events: EventsCfg = EventsCfg() + curriculum: CurriculumCfg = CurriculumCfg() + + def __post_init__(self): + super().__post_init__() diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_stones_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_stones_env.py new file mode 100644 index 0000000..edf696f --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/risky_terrains/stepping_stones_env.py @@ -0,0 +1,360 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +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 import ContactSensorCfg, RayCasterCfg, patterns +from isaaclab.terrains import TerrainImporterCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +import uwlab_tasks.manager_based.locomotion.risky_terrains.mdp as mdp + +from .config.terrains.terrain_cfg import RISKY_TERRAINS_CFG + + +@configclass +class SteppingStoneSceneCfg(InteractiveSceneCfg): + + # ground terrain + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="generator", + terrain_generator=RISKY_TERRAINS_CFG, + max_init_terrain_level=5, + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + ), + visual_material=sim_utils.MdlFileCfg( + mdl_path=f"{ISAACLAB_NUCLEUS_DIR}/Materials/TilesMarbleSpiderWhiteBrickBondHoned/TilesMarbleSpiderWhiteBrickBondHoned.mdl", + project_uvw=True, + texture_scale=(0.25, 0.25), + ), + debug_vis=False, + ) + + # 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", + ), + ) + + # robots + robot: ArticulationCfg = MISSING # type: ignore + + # sensors + height_scanner = RayCasterCfg( + prim_path="{ENV_REGEX_NS}/Robot/base", + offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), + ray_alignment="yaw", + pattern_cfg=patterns.GridPatternCfg(resolution=0.07, size=(1.68, 1.05)), + debug_vis=False, + mesh_prim_paths=["/World/ground"], + ) + contact_forces = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/.*", history_length=3, track_air_time=True, debug_vis=True + ) + + +@configclass +class ActionsCfg: + """Actions for the MDP.""" + + pass + + +@configclass +class CommandsCfg: + """Commands for the MDP.""" + + target_cmd = mdp.UniformPolarPose2dCommandCfg( + asset_name="robot", + resampling_time_range=(10, 10), + debug_vis=True, + simple_heading=False, + ranges=mdp.UniformPolarPose2dCommandCfg.Ranges(distance_range=(1.5, 4.9), heading=(-3.14, 3.14)), + ) + + +@configclass +class ObservationsCfg: + """Observations for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + 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.2, n_max=0.2)) + proj_gravity = ObsTerm(func=mdp.projected_gravity, noise=Unoise(n_min=-0.05, n_max=0.05)) + concatenate_cmd = ObsTerm(func=mdp.generated_commands, params={"command_name": "target_cmd"}) + time_left = ObsTerm(func=mdp.time_left) + joint_pos = ObsTerm(func=mdp.joint_pos, noise=Unoise(n_min=-0.01, n_max=0.01)) + joint_vel = ObsTerm(func=mdp.joint_vel, noise=Unoise(n_min=-1.5, n_max=1.5)) + last_actions = ObsTerm(func=mdp.last_action) + height_scan = ObsTerm( + func=mdp.height_scan, + params={"sensor_cfg": SceneEntityCfg(name="height_scanner")}, + noise=Unoise(n_min=-0.01, n_max=0.01), + clip=(-1.0, 1.0), + ) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventsCfg: + """Events for the MDP.""" + + # startup + physical_material = EventTerm( + func=mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": (0.8, 0.8), + "dynamic_friction_range": (0.6, 0.6), + "restitution_range": (0.0, 0.0), + "num_buckets": 64, + }, + ) + + add_base_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names="base"), + "mass_distribution_params": (-5.0, 5.0), + "operation": "add", + }, + ) + + # reset + base_external_force_torque = EventTerm( + func=mdp.apply_external_force_torque, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names="base"), + "force_range": (0.0, 0.0), + "torque_range": (-0.0, 0.0), + }, + ) + + reset_base = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5), "yaw": (-3.14, 3.14)}, + "velocity_range": { + "x": (-0.5, 0.5), + "y": (-0.5, 0.5), + "z": (-0.5, 0.5), + "roll": (-0.5, 0.5), + "pitch": (-0.5, 0.5), + "yaw": (-0.5, 0.5), + }, + }, + ) + + 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), + }, + ) + + # interval + # comment for pit and gap + # push_robot = EventTerm( + # func=mdp.push_by_setting_velocity, + # mode="interval", + # interval_range_s=(3.0, 4.5), + # params={"velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)}}, + # ) + + reset_episode_length = EventTerm( + func=mdp.reset_episode_length_s, mode="reset", params={"episode_length_s": (5.0, 7.0)} + ) + + +@configclass +class RewardsCfg: + """Rewards for the MDP.""" + + position_tracking = RewTerm(func=mdp.position_tracking, weight=10.0, params={"tr": 2.0}) + head_tracking = RewTerm(func=mdp.heading_tracking, weight=5, params={"d": 2.0, "tr": 4.0}) + early_termination = RewTerm( + func=mdp.is_terminated, + weight=-200, + ) + + # penalties + undesired_contact = RewTerm( + func=mdp.undesired_contacts, + weight=-1.0, + params={ + "sensor_cfg": SceneEntityCfg("contact_forces", body_names=[".*THIGH", ".*SHANK"]), + "threshold": 1.0, + }, + ) + + # illegal_contact_penalty = RewTerm( + # func=mdp.illegal_contact_penalty, + # weight=-1, + # params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names="base"), "threshold": 1.0}, + # ) + + joint_velocity = RewTerm(func=mdp.joint_vel_l2, weight=-0.001) + + joint_vel_limit = RewTerm(func=mdp.joint_vel_limit_pen, weight=-1.0, params={"limits_factor": 0.9}) + + base_accel = RewTerm( + func=mdp.base_accel_pen, + weight=-0.001, + params={"ratio": 0.02, "robot_cfg": SceneEntityCfg("robot", body_names="base")}, + ) + + feet_accel = RewTerm( + func=mdp.feet_accel_l1_pen, weight=-0.0005, params={"robot_cfg": SceneEntityCfg("robot", body_names=".*FOOT")} + ) + + action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=-0.01) + + joint_torque_l2 = RewTerm(func=mdp.joint_torques_l2, weight=-1e-5) + + joint_torque_limits = RewTerm(func=mdp.torque_limits, weight=-0.2, params={"ratio": 1.0}) + + contact_force_pen = RewTerm( + func=mdp.contact_forces_pen, + weight=-2.5e-5, + params={"threshold": 700, "sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*FOOT")}, + ) + + dont_wait = RewTerm( + func=mdp.dont_wait, + weight=-1.0, + params={"robot_cfg": SceneEntityCfg("robot"), "d": 1.0, "velocity_threshold": 0.2}, + ) + + move_in_dir = RewTerm( + func=mdp.move_in_dir, + weight=1.0, + params={ + "max_iter": 150, + "robot_cfg": SceneEntityCfg("robot"), + }, + ) + + stand_still = RewTerm( + func=mdp.stand_still, + weight=-1.0, + params={"robot_cfg": SceneEntityCfg("robot"), "d": 0.5, "tr": 1.0, "phi": 0.5}, + ) + + # curiosity_reward = RewTerm( + # func=mdp.CuriosityReward, + # weight=1.0, + # params={ + # "optimization_weight": 1.0, + # "lr": 1e-3 + # } + # ) + + +@configclass +class TerminationsCfg: + """Terminations for the MDP.""" + + time_out = DoneTerm(func=mdp.custom_time_out, time_out=True) + + base_contact = DoneTerm( + func=mdp.illegal_contact, + params={ + "sensor_cfg": SceneEntityCfg("contact_forces", body_names="base"), + "threshold": 1.0, + }, + ) + + ill_posture = DoneTerm(func=mdp.bad_orientation, params={"limit_angle": 0.75}) + + +@configclass +class CurriculumCfg: + """Curriculum for the MDP.""" + + terrain_levels = CurrTerm( + func=mdp.terrain_levels_risky, # type: ignore + params={ + "demotion_fraction": 0.00, + }, + ) + + +@configclass +class SteppingStoneLocomotionEnvCfg(ManagerBasedRLEnvCfg): + scene: SteppingStoneSceneCfg = SteppingStoneSceneCfg(num_envs=4096, env_spacing=2.5) + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventsCfg = EventsCfg() + curriculum: CurriculumCfg = CurriculumCfg() + viewer: ViewerCfg = ViewerCfg(eye=(1.0, 2.0, 2.0), origin_type="asset_body", asset_name="robot", body_name="base") + + def __post_init__(self): + self.is_finite_horizon = True + self.decimation = 2 + self.episode_length_s = 6.0 + + self.sim.dt = 0.01 + self.sim.render_interval = self.decimation + self.sim.disable_contact_processing = True + self.sim.physics_material = self.scene.terrain.physics_material + self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**24 + self.sim.physx.gpu_found_lost_pairs_capacity = 2**24 + self.sim.physx.gpu_collision_stack_size = 2**27 + self.sim.physx.gpu_max_rigid_patch_count = 6 * 2**15 + + self.viewer.resolution = (1920, 1080) + + # update sensor update periods + # we tick all the sensors based on the smallest update period (physics update period) + if self.scene.height_scanner is not None: + self.scene.height_scanner.update_period = self.decimation * self.sim.dt + if self.scene.contact_forces is not None: + self.scene.contact_forces.update_period = self.sim.dt + + # check if terrain levels curriculum is enabled - if so, enable curriculum for terrain generator + # this generates terrains with increasing difficulty and is useful for training + if getattr(self.curriculum, "terrain_levels", None) is not None: + if self.scene.terrain.terrain_generator is not None: + self.scene.terrain.terrain_generator.curriculum = True + else: + if self.scene.terrain.terrain_generator is not None: + self.scene.terrain.terrain_generator.curriculum = False diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/__init__.py index b784a89..6f699f5 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/__init__.py index d3ddfa9..2e0cd43 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py index 1cecaa5..9c1e144 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -13,7 +13,7 @@ gym.register( id="UW-Velocity-Flat-Anymal-C-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": flat_env_cfg.AnymalCRoughEnvCfg, @@ -25,7 +25,7 @@ gym.register( id="UW-Velocity-Flat-Anymal-C-Play-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": flat_env_cfg.AnymalCFlatEnvCfg_PLAY, @@ -38,7 +38,7 @@ gym.register( id="UW-Velocity-Rough-Anymal-C-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": rough_env_cfg.AnymalCRoughEnvCfg, @@ -50,7 +50,7 @@ gym.register( id="UW-Velocity-Rough-Anymal-C-Play-v0", - entry_point="uwlab.envs:DataManagerBasedRLEnv", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": rough_env_cfg.AnymalCRoughEnvCfg_PLAY, diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml index c472ce6..88f110e 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_flat_ppo_cfg.yaml @@ -1,3 +1,8 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + params: seed: 42 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml index 042799d..5ec47c6 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rl_games_rough_ppo_cfg.yaml @@ -1,3 +1,8 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + params: seed: 42 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_cfg.py index d677fa7..aec0bed 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/rsl_rl_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml index a642556..3deb908 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_flat_ppo_cfg.yaml @@ -1,3 +1,8 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + seed: 42 # Models are instantiated using skrl's model instantiator utility diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml index 1e33f06..1fb80ae 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/agents/skrl_rough_ppo_cfg.yaml @@ -1,3 +1,8 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + seed: 42 # Models are instantiated using skrl's model instantiator utility diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py index acce623..9a61fd8 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/flat_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py index f9abfd0..3a1ba22 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/anymal_c/rough_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py index 815b09a..bd5c0d2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py index 704e0d4..b040e60 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/agents/rsl_rl_ppo_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py index 8a5f936..0cb964a 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/flat_env_cfg.py @@ -1,10 +1,12 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import isaaclab.sim as sim_utils import isaaclab.terrains as terrain_gen +import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp +import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab.envs import ViewerCfg from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationGroupCfg as ObsGroup @@ -15,9 +17,6 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise - -import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp -import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg ## diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py index 8a1ad1b..844793e 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py index 74db481..fd67152 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot/rough_env_cfg.py @@ -1,8 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause +import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp +import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab.envs import ViewerCfg from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationGroupCfg as ObsGroup @@ -11,9 +13,6 @@ from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise - -import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp -import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg ## diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/__init__.py index e9e05fd..1f40976 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/rsl_rl_ppo_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/rsl_rl_ppo_cfg.py index 37c58aa..d6177fd 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/rsl_rl_ppo_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/agents/rsl_rl_ppo_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py index e585352..96c66ee 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/flat_env_cfg.py @@ -1,10 +1,12 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import isaaclab.sim as sim_utils import isaaclab.terrains as terrain_gen +import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp +import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab.envs import ViewerCfg from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationGroupCfg as ObsGroup @@ -15,12 +17,10 @@ from isaaclab.utils import configclass from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise -from uwlab.envs.mdp import DefaultJointPositionStaticActionCfg - -import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp -import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg +from uwlab.envs.mdp import DefaultJointPositionStaticActionCfg + ## # Pre-defined configs ## diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/mdp/__init__.py index 8a1ad1b..844793e 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/mdp/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py index 2c950ab..49510a7 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/config/spot_with_arm/rough_env_cfg.py @@ -1,8 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause +import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp +import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab.envs import ViewerCfg from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationGroupCfg as ObsGroup @@ -11,12 +13,10 @@ from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise -from uwlab.envs.mdp import DefaultJointPositionStaticActionCfg - -import isaaclab_tasks.manager_based.locomotion.velocity.config.spot.mdp as spot_mdp -import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab_tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg +from uwlab.envs.mdp import DefaultJointPositionStaticActionCfg + ## # Pre-defined configs ## diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/__init__.py index 1b5fe13..936ec99 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -6,6 +6,7 @@ """This sub-module contains the functions that are specific to the locomotion environments.""" from isaaclab.envs.mdp import * + from uwlab.envs.mdp import * # noqa: F401, F403 from .curriculums import * # noqa: F401, F403 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py index 1a29e24..a737394 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/curriculums.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py index 1a68d14..07df109 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,7 +10,7 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.sensors import ContactSensor -from isaaclab.utils.math import quat_rotate_inverse, yaw_quat +from isaaclab.utils.math import quat_apply_inverse, yaw_quat if TYPE_CHECKING: from isaaclab.envs import ManagerBasedRLEnv @@ -76,7 +76,7 @@ def track_lin_vel_xy_yaw_frame_exp( """Reward tracking of linear velocity commands (xy axes) in the gravity aligned robot frame using exponential kernel.""" # extract the used quantities (to enable type-hinting) asset = env.scene[asset_cfg.name] - vel_yaw = quat_rotate_inverse(yaw_quat(asset.data.root_quat_w), asset.data.root_lin_vel_w[:, :3]) + vel_yaw = quat_apply_inverse(yaw_quat(asset.data.root_quat_w), asset.data.root_lin_vel_w[:, :3]) lin_vel_error = torch.sum( torch.square(env.command_manager.get_command(command_name)[:, :2] - vel_yaw[:, :2]), dim=1 ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py index e1fcfd2..94d9872 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -7,6 +7,7 @@ from dataclasses import MISSING import isaaclab.sim as sim_utils +import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.managers import CurriculumTermCfg as CurrTerm @@ -16,14 +17,12 @@ 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 ContactSensorCfg, RayCasterCfg, patterns +from isaaclab.terrains import TerrainImporterCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise -from uwlab.scene import InteractiveSceneCfg -from uwlab.terrains import TerrainImporterCfg - -import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp from uwlab.terrains.config.rough import ROUGH_TERRAINS_CFG # isort: skip @@ -58,7 +57,7 @@ class MySceneCfg(InteractiveSceneCfg): height_scanner = RayCasterCfg( prim_path="{ENV_REGEX_NS}/Robot/base", offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), - attach_yaw_only=True, + ray_alignment="yaw", pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=(1.6, 1.0)), debug_vis=False, mesh_prim_paths=["/World/ground"], diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/__init__.py index 1e55ad1..4d58247 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/__init__.py index 2863ad6..da737f3 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py index dc959f3..29bd36b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/assembly_keypoints.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/__init__.py index cd7e706..94bc589 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/__init__.py index 095f51c..4d155cf 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/rsl_rl_ppo_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/rsl_rl_ppo_cfg.py index 1e60f03..46d1b49 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/rsl_rl_ppo_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/agents/rsl_rl_ppo_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/ik_del_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/ik_del_env_cfg.py index 443b268..bafa21f 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/ik_del_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/ik_del_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/joint_pos_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/joint_pos_env_cfg.py index a9baaac..44a3a95 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/joint_pos_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/config/franka/joint_pos_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py index f2389b6..5945c65 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_assets_cfg.py @@ -1,10 +1,8 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg @@ -12,6 +10,8 @@ # This is where we will get the Robot that we want to use from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + ASSET_DIR = f"{ISAACLAB_NUCLEUS_DIR}/Factory" diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py index c1fbb81..99199d6 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/factory_env_base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/gearmesh_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/gearmesh_env_cfg.py index ad59458..2cd6784 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/gearmesh_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/gearmesh_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/__init__.py index ef08ed5..4b4b945 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/actions_cfg_nist.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/actions_cfg_nist.py index 03f6f98..014295a 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/actions_cfg_nist.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/actions_cfg_nist.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py index cd8da02..0ea9aea 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/actions/task_space_actions_nist.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -10,10 +10,9 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -import omni.log - import isaaclab.utils.math as math_utils import isaaclab.utils.string as string_utils +import omni.log from isaaclab.assets.articulation import Articulation from isaaclab.controllers.differential_ik import DifferentialIKController from isaaclab.managers.action_manager import ActionTerm diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py index db599c0..5c93a97 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/events.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -13,6 +13,7 @@ from isaaclab.envs.mdp.actions.task_space_actions import DifferentialInverseKinematicsAction from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg from isaaclab.utils import math as math_utils + from uwlab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg from ..assembly_keypoints import KEYPOINTS_NISTBOARD diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py index 2009303..2f3fa7a 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/observations.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/rewards.py index 1c5eacc..6ad0c39 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/nutthread_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/nutthread_env_cfg.py index 958f999..3687d3d 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/nutthread_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/nutthread_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/peginsert_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/peginsert_env_cfg.py index 54a55d6..f87cd0e 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/peginsert_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/factory_extension/peginsert_env_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/__init__.py new file mode 100644 index 0000000..f26c228 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +"""OmniReset Environments.""" diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/assembly_keypoints.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/assembly_keypoints.py new file mode 100644 index 0000000..5eaea63 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/assembly_keypoints.py @@ -0,0 +1,52 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +import isaaclab.utils.math as math_utils +from isaaclab.utils import configclass + +if TYPE_CHECKING: + from isaaclab.assets import Articulation, RigidObject + + +@configclass +class Offset: + pos: tuple[float, float, float] = (0.0, 0.0, 0.0) + quat: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) + + @property + def pose(self) -> tuple[float, float, float, float, float, float, float]: + return self.pos + self.quat + + def apply(self, root: RigidObject | Articulation) -> tuple[torch.Tensor, torch.Tensor]: + data = root.data.root_pos_w + pos_w, quat_w = math_utils.combine_frame_transforms( + root.data.root_pos_w, + root.data.root_quat_w, + torch.tensor(self.pos).to(data.device).repeat(data.shape[0], 1), + torch.tensor(self.quat).to(data.device).repeat(data.shape[0], 1), + ) + return pos_w, quat_w + + def combine(self, pos: torch.Tensor, quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + num_data = pos.shape[0] + device = pos.device + return math_utils.combine_frame_transforms( + pos, + quat, + torch.tensor(self.pos).to(device).repeat(num_data, 1), + torch.tensor(self.quat).to(device).repeat(num_data, 1), + ) + + def subtract(self, pos_w: torch.Tensor, quat_w: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + offset_pos = torch.tensor(self.pos).to(pos_w.device).repeat(pos_w.shape[0], 1) + offset_quat = torch.tensor(self.quat).to(pos_w.device).repeat(pos_w.shape[0], 1) + inv_offset_pos = -math_utils.quat_apply(math_utils.quat_inv(offset_quat), offset_pos) + inv_offset_quat = math_utils.quat_inv(offset_quat) + return math_utils.combine_frame_transforms(pos_w, quat_w, inv_offset_pos, inv_offset_quat) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/__init__.py new file mode 100644 index 0000000..59a500c --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for OmniReset 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/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/__init__.py new file mode 100644 index 0000000..e146a40 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/__init__.py @@ -0,0 +1,103 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Reset states tasks for IsaacLab.""" + +import gymnasium as gym + +from . import agents + +# Register the partial assemblies environment +gym.register( + id="OmniReset-PartialAssemblies-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={"env_cfg_entry_point": f"{__name__}.partial_assemblies_cfg:PartialAssembliesCfg"}, + disable_env_checker=True, +) + +# Register the grasp sampling environment +gym.register( + id="OmniReset-Robotiq2f85-GraspSampling-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={"env_cfg_entry_point": f"{__name__}.grasp_sampling_cfg:Robotiq2f85GraspSamplingCfg"}, + disable_env_checker=True, +) + +# Register reset states environments +gym.register( + id="OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEAnywhere-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={"env_cfg_entry_point": f"{__name__}.reset_states_cfg:ObjectAnywhereEEAnywhereResetStatesCfg"}, +) + +gym.register( + id="OmniReset-UR5eRobotiq2f85-ObjectRestingEEGrasped-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={"env_cfg_entry_point": f"{__name__}.reset_states_cfg:ObjectRestingEEGraspedResetStatesCfg"}, +) + +gym.register( + id="OmniReset-UR5eRobotiq2f85-ObjectAnywhereEEGrasped-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={"env_cfg_entry_point": f"{__name__}.reset_states_cfg:ObjectAnywhereEEGraspedResetStatesCfg"}, +) + +gym.register( + id="OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEAnywhere-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={"env_cfg_entry_point": f"{__name__}.reset_states_cfg:ObjectPartiallyAssembledEEAnywhereResetStatesCfg"}, +) + +gym.register( + id="OmniReset-UR5eRobotiq2f85-ObjectPartiallyAssembledEEGrasped-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={"env_cfg_entry_point": f"{__name__}.reset_states_cfg:ObjectPartiallyAssembledEEGraspedResetStatesCfg"}, +) + +# Register RL state environments +gym.register( + id="OmniReset-Ur5eRobotiq2f85-RelJointPos-State-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.rl_state_cfg:Ur5eRobotiq2f85RelJointPosTrainCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:Base_PPORunnerCfg", + }, +) + +gym.register( + id="OmniReset-Ur5eRobotiq2f85-RelJointPos-State-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.rl_state_cfg:Ur5eRobotiq2f85RelJointPosEvalCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:Base_PPORunnerCfg", + }, +) + +gym.register( + id="OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.rl_state_cfg:Ur5eRobotiq2f85RelCartesianOSCTrainCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:Base_PPORunnerCfg", + }, +) + +gym.register( + id="OmniReset-Ur5eRobotiq2f85-RelCartesianOSC-State-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.rl_state_cfg:Ur5eRobotiq2f85RelCartesianOSCEvalCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_cfg:Base_PPORunnerCfg", + }, +) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/actions.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/actions.py new file mode 100644 index 0000000..36b6cc6 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/actions.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from isaaclab.controllers import OperationalSpaceControllerCfg +from isaaclab.utils import configclass + +from uwlab_assets.robots.ur5e_robotiq_gripper import EXPLICIT_UR5E_ROBOTIQ_2F85 +from uwlab_assets.robots.ur5e_robotiq_gripper.actions import ROBOTIQ_COMPLIANT_JOINTS, ROBOTIQ_GRIPPER_BINARY_ACTIONS + +from uwlab_tasks.manager_based.manipulation.reset_states.mdp.utils import read_metadata_from_usd_directory + +from ...mdp.actions.actions_cfg import TransformedOperationalSpaceControllerActionCfg + +UR5E_ROBOTIQ_2F85_RELATIVE_OSC = TransformedOperationalSpaceControllerActionCfg( + asset_name="robot", + joint_names=["shoulder.*", "elbow.*", "wrist.*"], + body_name="robotiq_base_link", + body_offset=TransformedOperationalSpaceControllerActionCfg.OffsetCfg( + pos=(0.1345, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0) + ), + action_root_offset=TransformedOperationalSpaceControllerActionCfg.OffsetCfg( + pos=read_metadata_from_usd_directory(EXPLICIT_UR5E_ROBOTIQ_2F85.spawn.usd_path).get("offset").get("pos"), + rot=read_metadata_from_usd_directory(EXPLICIT_UR5E_ROBOTIQ_2F85.spawn.usd_path).get("offset").get("quat"), + ), + scale_xyz_axisangle=(0.02, 0.02, 0.02, 0.02, 0.02, 0.2), + controller_cfg=OperationalSpaceControllerCfg( + target_types=["pose_rel"], + impedance_mode="fixed", + inertial_dynamics_decoupling=False, + partial_inertial_dynamics_decoupling=False, + gravity_compensation=False, + motion_stiffness_task=(200.0, 200.0, 200.0, 3.0, 3.0, 3.0), + motion_damping_ratio_task=(3.0, 3.0, 3.0, 1.0, 1.0, 1.0), + nullspace_control="none", + ), + position_scale=1.0, + orientation_scale=1.0, + stiffness_scale=1.0, + damping_ratio_scale=1.0, +) + + +@configclass +class Ur5eRobotiq2f85RelativeOSCAction: + arm = UR5E_ROBOTIQ_2F85_RELATIVE_OSC + gripper = ROBOTIQ_GRIPPER_BINARY_ACTIONS + compliant_joints = ROBOTIQ_COMPLIANT_JOINTS diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/agents/__init__.py new file mode 100644 index 0000000..26ab8ff --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/agents/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from . import rsl_rl_cfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/agents/rsl_rl_cfg.py new file mode 100644 index 0000000..61fe53b --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/agents/rsl_rl_cfg.py @@ -0,0 +1,48 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoAlgorithmCfg + +from uwlab_rl.rsl_rl.rl_cfg import RslRlFancyActorCriticCfg + + +def my_experts_observation_func(env): + obs = env.unwrapped.obs_buf["expert_obs"] + return obs + + +@configclass +class Base_PPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 32 + max_iterations = 40000 + save_interval = 100 + resume = False + experiment_name = "ur5e_robotiq_2f85_reset_states_agent" + policy = RslRlFancyActorCriticCfg( + init_noise_std=1.0, + actor_obs_normalization=True, + critic_obs_normalization=True, + actor_hidden_dims=[512, 256, 128, 64], + critic_hidden_dims=[512, 256, 128, 64], + activation="elu", + noise_std_type="gsde", + state_dependent_std=False, + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + normalize_advantage_per_mini_batch=False, + 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.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/grasp_sampling_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/grasp_sampling_cfg.py new file mode 100644 index 0000000..73781da --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/grasp_sampling_cfg.py @@ -0,0 +1,218 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import numpy as np + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +from isaaclab.managers import EventTermCfg as EventTerm +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 + +import uwlab_assets.robots.ur5e_robotiq_gripper as ur5e_robotiq_gripper +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + +from ... import mdp as task_mdp + +OBJECT_SPAWN_HEIGHT = 0.5 + + +@configclass +class GraspSamplingSceneCfg(InteractiveSceneCfg): + """Scene configuration for grasp sampling environment.""" + + robot = ur5e_robotiq_gripper.ROBOTIQ_2F85.replace(prim_path="{ENV_REGEX_NS}/Robot") + + object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Object", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd", + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0, disable_gravity=False + ), + # assume very light + mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + # Environment + ground = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0)), + spawn=sim_utils.GroundPlaneCfg(), + ) + + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=1000.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + +@configclass +class GraspSamplingEventCfg: + """Configuration for grasp sampling randomization.""" + + reset_object_position = EventTerm( + func=task_mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": { + "x": (0.0, 0.0), + "y": (0.0, 0.0), + "z": (0.3, 0.3), + "roll": (0.0, 0.0), + "pitch": (0.0, 0.0), + "yaw": (0.0, 0.0), + }, + "velocity_range": {}, + "asset_cfg": SceneEntityCfg("object"), + }, + ) + + grasp_sampling = EventTerm( + func=task_mdp.grasp_sampling_event, + mode="reset", + params={ + "object_cfg": SceneEntityCfg("object"), + "gripper_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "num_candidates": 1e6, + "num_standoff_samples": 32, + "num_orientations": 16, + "lateral_sigma": 0.0, + "visualize_grasps": False, + "visualization_scale": 0.01, + }, + ) + + global_physics_control_event = EventTerm( + func=task_mdp.global_physics_control_event, + mode="interval", + interval_range_s=(0.1, 0.1), + params={ + "gravity_on_interval": (1.0, np.inf), + "force_torque_on_interval": (1.0, 2.0), + "force_torque_asset_cfgs": [SceneEntityCfg("object")], + "force_torque_magnitude": 0.01, + }, + ) + + +@configclass +class GraspSamplingTerminationCfg: + """Configuration for grasp sampling termination conditions.""" + + time_out = DoneTerm(func=task_mdp.time_out, time_out=True) + + success = DoneTerm( + func=task_mdp.check_grasp_success, + params={ + "object_cfg": SceneEntityCfg("object"), + "gripper_cfg": SceneEntityCfg("robot"), + "collision_analyzer_cfg": task_mdp.CollisionAnalyzerCfg( + num_points=1024, + max_dist=0.5, + min_dist=-0.0005, + asset_cfg=SceneEntityCfg("robot"), + obstacle_cfgs=[SceneEntityCfg("object")], + ), + "max_pos_deviation": OBJECT_SPAWN_HEIGHT / 2, + "pos_z_threshold": OBJECT_SPAWN_HEIGHT / 2, + }, + time_out=True, + ) + + +@configclass +class GraspSamplingObservationsCfg: + """Configuration for grasp sampling observations.""" + + pass + + +@configclass +class GraspSamplingRewardsCfg: + """Configuration for grasp sampling rewards.""" + + pass + + +def make_object(usd_path: str): + return RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/InsertiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=usd_path, + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=False, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +variants = { + "scene.object": { + "fbleg": make_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/SquareLeg/square_leg.usd"), + "fbdrawerbottom": make_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/DrawerBottom/drawer_bottom.usd" + ), + "peg": make_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd"), + } +} + + +@configclass +class Robotiq2f85GraspSamplingCfg(ManagerBasedRLEnvCfg): + """Configuration for grasp sampling environment with Robotiq 2F85 gripper.""" + + scene: GraspSamplingSceneCfg = GraspSamplingSceneCfg(num_envs=1, env_spacing=1.5) + events: GraspSamplingEventCfg = GraspSamplingEventCfg() + terminations: GraspSamplingTerminationCfg = GraspSamplingTerminationCfg() + observations: GraspSamplingObservationsCfg = GraspSamplingObservationsCfg() + actions: ur5e_robotiq_gripper.Robotiq2f85BinaryGripperAction = ur5e_robotiq_gripper.Robotiq2f85BinaryGripperAction() + rewards: GraspSamplingRewardsCfg = GraspSamplingRewardsCfg() + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 0.0, 0.75), origin_type="world", env_index=0, asset_name="robot") + variants = variants + + def __post_init__(self): + self.decimation = 12 + self.episode_length_s = 4.0 + # simulation settings + self.sim.dt = 1 / 120.0 + + # Contact and solver settings + self.sim.physx.solver_type = 1 + self.sim.physx.max_position_iteration_count = 192 + self.sim.physx.max_velocity_iteration_count = 1 + self.sim.physx.bounce_threshold_velocity = 0.02 + self.sim.physx.friction_offset_threshold = 0.01 + self.sim.physx.friction_correlation_distance = 0.0005 + + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 + self.sim.physx.gpu_max_rigid_contact_count = 2**23 + self.sim.physx.gpu_max_rigid_patch_count = 2**23 + self.sim.physx.gpu_collision_stack_size = 2**31 + + # Render settings + self.sim.render.enable_dlssg = True + self.sim.render.enable_ambient_occlusion = True + self.sim.render.enable_reflections = True + self.sim.render.enable_dl_denoiser = True diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/partial_assemblies_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/partial_assemblies_cfg.py new file mode 100644 index 0000000..70590c5 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/partial_assemblies_cfg.py @@ -0,0 +1,285 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +from isaaclab.managers import EventTermCfg as EventTerm +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 uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + +from ... import mdp as task_mdp + +OBJECT_SPAWN_HEIGHT = 0.5 + + +@configclass +class PartialAssembliesSceneCfg(InteractiveSceneCfg): + """Scene configuration for partial assemblies environment.""" + + insertive_object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/InsertiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd", + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=True, + kinematic_enabled=False, + ), + # assume very light + mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT * 2), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + receptive_object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ReceptiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/PegHole/peg_hole.usd", + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=True, + ), + # since kinematic_enabled=True, mass does not matter + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + # Environment + ground = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0)), + spawn=sim_utils.GroundPlaneCfg(), + ) + + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=1000.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + +@configclass +class PartialAssembliesEventCfg: + """Configuration for partial assemblies randomization.""" + + # Low friction so that the object can around + insertive_object_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, + mode="startup", + params={ + "static_friction_range": (0.0, 0.0), + "dynamic_friction_range": (0.0, 0.0), + "restitution_range": (0.0, 0.0), + "num_buckets": 1, + "asset_cfg": SceneEntityCfg("insertive_object"), + "make_consistent": True, + }, + ) + + receptive_object_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, + mode="startup", + params={ + "static_friction_range": (0.0, 0.0), + "dynamic_friction_range": (0.0, 0.0), + "restitution_range": (0.0, 0.0), + "num_buckets": 1, + "asset_cfg": SceneEntityCfg("receptive_object"), + "make_consistent": True, + }, + ) + + partial_assembly_sampling = EventTerm( + func=task_mdp.assembly_sampling_event, + mode="reset", + params={ + "receptive_object_cfg": SceneEntityCfg("receptive_object"), + "insertive_object_cfg": SceneEntityCfg("insertive_object"), + }, + ) + + apply_forces = EventTerm( + func=task_mdp.apply_external_force_torque, + mode="interval", + interval_range_s=(1 / 120, 1 / 120), + params={ + "asset_cfg": SceneEntityCfg("insertive_object"), + "force_range": (-0.005, 0.005), + "torque_range": (-0.005, 0.005), + }, + ) + + # Collect pose data from environments with positive rewards + pose_data_collection = EventTerm( + func=task_mdp.pose_logging_event, + mode="interval", + interval_range_s=(1 / 120, 1 / 120), + params={ + "receptive_object_cfg": SceneEntityCfg("receptive_object"), + "insertive_object_cfg": SceneEntityCfg("insertive_object"), + }, + ) + + +@configclass +class PartialAssembliesTerminationCfg: + """Configuration for partial assemblies termination conditions.""" + + time_out = DoneTerm(func=task_mdp.time_out, time_out=True) + + obb_no_overlap = DoneTerm( + func=task_mdp.check_obb_no_overlap_termination, + params={ + "insertive_object_cfg": SceneEntityCfg("insertive_object"), + "enable_visualization": False, + }, + time_out=True, + ) + + +@configclass +class PartialAssembliesObservationsCfg: + """Configuration for partial assemblies observations.""" + + pass + + +@configclass +class PartialAssembliesRewardsCfg: + """Configuration for partial assemblies rewards.""" + + collision_free = RewTerm( + func=task_mdp.collision_free, + params={ + "collision_analyzer_cfg": task_mdp.CollisionAnalyzerCfg( + num_points=1024, + max_dist=0.5, + min_dist=-0.0005, + asset_cfg=SceneEntityCfg("insertive_object"), + obstacle_cfgs=[SceneEntityCfg("receptive_object")], + ) + }, + weight=1.0, + ) + + +@configclass +class PartialAssembliesActionsCfg: + """Configuration for partial assemblies actions.""" + + pass + + +def make_insertive_object(usd_path: str): + return RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/InsertiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=usd_path, + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=True, + kinematic_enabled=False, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT * 2), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +def make_receptive_object(usd_path: str): + return RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ReceptiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=usd_path, + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=True, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, OBJECT_SPAWN_HEIGHT), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +variants = { + "scene.insertive_object": { + "fbleg": make_insertive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/SquareLeg/square_leg.usd"), + "fbdrawerbottom": make_insertive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/DrawerBottom/drawer_bottom.usd" + ), + "peg": make_insertive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd"), + }, + "scene.receptive_object": { + "fbtabletop": make_receptive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/SquareTableTop/square_table_top.usd" + ), + "fbdrawerbox": make_receptive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/DrawerBox/drawer_box.usd" + ), + "peghole": make_receptive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/PegHole/peg_hole.usd"), + }, +} + + +@configclass +class PartialAssembliesCfg(ManagerBasedRLEnvCfg): + """Configuration for partial assemblies environment without robot.""" + + scene: PartialAssembliesSceneCfg = PartialAssembliesSceneCfg(num_envs=1, env_spacing=2.0) + events: PartialAssembliesEventCfg = PartialAssembliesEventCfg() + terminations: PartialAssembliesTerminationCfg = PartialAssembliesTerminationCfg() + observations: PartialAssembliesObservationsCfg = PartialAssembliesObservationsCfg() + actions: PartialAssembliesActionsCfg = PartialAssembliesActionsCfg() + rewards: PartialAssembliesRewardsCfg = PartialAssembliesRewardsCfg() + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 0.0, 0.75), origin_type="world", env_index=0, asset_name="receptive_object") + variants = variants + + def __post_init__(self): + self.decimation = 1 # We want to save fine-grained poses + self.episode_length_s = 4.0 + # simulation settings + self.sim.dt = 1 / 120.0 + + # Contact and solver settings + self.sim.physx.solver_type = 1 + self.sim.physx.max_position_iteration_count = 192 + self.sim.physx.max_velocity_iteration_count = 1 + self.sim.physx.bounce_threshold_velocity = 0.02 + self.sim.physx.friction_offset_threshold = 0.01 + self.sim.physx.friction_correlation_distance = 0.0005 + + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 + self.sim.physx.gpu_max_rigid_contact_count = 2**23 + self.sim.physx.gpu_max_rigid_patch_count = 2**23 + self.sim.physx.gpu_collision_stack_size = 2**31 + + # Render settings + self.sim.render.enable_dlssg = True + self.sim.render.enable_ambient_occlusion = True + self.sim.render.enable_reflections = True + self.sim.render.enable_dl_denoiser = True diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/reset_states_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/reset_states_cfg.py new file mode 100644 index 0000000..9e3683d --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/reset_states_cfg.py @@ -0,0 +1,590 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import numpy as np +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +from isaaclab.managers import EventTermCfg as EventTerm +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 uwlab_assets import UWLAB_CLOUD_ASSETS_DIR +from uwlab_assets.robots.ur5e_robotiq_gripper import EXPLICIT_UR5E_ROBOTIQ_2F85 + +from uwlab_tasks.manager_based.manipulation.reset_states.config.ur5e_robotiq_2f85.actions import ( + Ur5eRobotiq2f85RelativeOSCAction, +) + +from ... import mdp as task_mdp + + +@configclass +class ResetStatesSceneCfg(InteractiveSceneCfg): + """Scene configuration for reset states environment.""" + + robot = EXPLICIT_UR5E_ROBOTIQ_2F85.replace(prim_path="{ENV_REGEX_NS}/Robot") + + insertive_object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/InsertiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd", + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=False, + ), + # assume very light + mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + receptive_object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ReceptiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/PegHole/peg_hole.usd", + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + # receptive object does not move + kinematic_enabled=True, + ), + # since kinematic_enabled=True, mass does not matter + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + # Environment + table = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Table", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.881), rot=(0.707, 0.0, 0.0, -0.707)), + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention/pat_vention.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), + ), + ) + + ur5_metal_support = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/UR5MetalSupport", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0, -0.013), rot=(1.0, 0.0, 0.0, 0.0)), + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention2/Ur5MetalSupport/ur5plate.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), + ), + ) + + ground = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, -0.868)), + spawn=sim_utils.GroundPlaneCfg(), + ) + + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=10000.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + +@configclass +class ResetStatesBaseEventCfg: + """Configuration for randomization.""" + + # startup: low friction to avoid slip + reset_robot_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "static_friction_range": (0.3, 0.3), + "dynamic_friction_range": (0.2, 0.2), + "restitution_range": (0.0, 0.0), + "num_buckets": 1, + "asset_cfg": SceneEntityCfg("robot"), + "make_consistent": True, + }, + ) + + insertive_object_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "static_friction_range": (0.3, 0.3), + "dynamic_friction_range": (0.2, 0.2), + "restitution_range": (0.0, 0.0), + "num_buckets": 1, + "asset_cfg": SceneEntityCfg("insertive_object"), + "make_consistent": True, + }, + ) + + receptive_object_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "static_friction_range": (0.3, 0.3), + "dynamic_friction_range": (0.2, 0.2), + "restitution_range": (0.0, 0.0), + "num_buckets": 1, + "asset_cfg": SceneEntityCfg("receptive_object"), + "make_consistent": True, + }, + ) + + # reset + + reset_everything = EventTerm(func=task_mdp.reset_scene_to_default, mode="reset", params={}) + + reset_robot_pose = EventTerm( + func=task_mdp.reset_root_states_uniform, + mode="reset", + params={ + "pose_range": { + "x": (-0.01, 0.01), + "y": (-0.059, -0.019), + "z": (-0.01, 0.01), + "roll": (0.0, 0.0), + "pitch": (0.0, 0.0), + "yaw": (0.0, 0.0), + }, + "velocity_range": {}, + "asset_cfgs": {"robot": SceneEntityCfg("robot"), "ur5_metal_support": SceneEntityCfg("ur5_metal_support")}, + }, + ) + + reset_receptive_object_pose = EventTerm( + func=task_mdp.reset_root_states_uniform, + mode="reset", + params={ + "pose_range": { + "x": (0.3, 0.55), + "y": (-0.1, 0.3), + "z": (0.0, 0.01), + "roll": (0.0, 0.0), + "pitch": (0.0, 0.0), + "yaw": (-np.pi / 12, np.pi / 12), + }, + "velocity_range": {}, + "asset_cfgs": {"receptive_object": SceneEntityCfg("receptive_object")}, + "offset_asset_cfg": SceneEntityCfg("ur5_metal_support"), + "use_bottom_offset": True, + }, + ) + + +@configclass +class ObjectAnywhereEEAnywhereEventCfg(ResetStatesBaseEventCfg): + reset_insertive_object_pose = EventTerm( + func=task_mdp.reset_root_states_uniform, + mode="reset", + params={ + "pose_range": { + "x": (0.3, 0.55), + "y": (-0.1, 0.3), + "z": (0.0, 0.3), + "roll": (-np.pi, np.pi), + "pitch": (-np.pi, np.pi), + "yaw": (-np.pi, np.pi), + }, + "velocity_range": {}, + "asset_cfgs": {"insertive_object": SceneEntityCfg("insertive_object")}, + "offset_asset_cfg": SceneEntityCfg("ur5_metal_support"), + "use_bottom_offset": True, + }, + ) + + reset_end_effector_pose = EventTerm( + func=task_mdp.reset_end_effector_round_fixed_asset, + mode="reset", + params={ + "fixed_asset_cfg": SceneEntityCfg("robot"), + "fixed_asset_offset": None, + "pose_range_b": { + "x": (0.3, 0.7), + "y": (-0.4, 0.4), + "z": (0.0, 0.5), + "roll": (0.0, 0.0), + "pitch": (np.pi / 4, 3 * np.pi / 4), + "yaw": (np.pi / 2, 3 * np.pi / 2), + }, + "robot_ik_cfg": SceneEntityCfg( + "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" + ), + }, + ) + + +@configclass +class ObjectRestingEEGraspedEventCfg(ResetStatesBaseEventCfg): + reset_insertive_object_pose_from_reset_states = EventTerm( + func=task_mdp.MultiResetManager, + mode="reset", + params={ + "base_paths": [f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/Resets/Assemblies/ObjectAnywhereEEAnywhere"], + "probs": [1.0], + }, + ) + + reset_end_effector_pose_from_grasp_dataset = EventTerm( + func=task_mdp.reset_end_effector_from_grasp_dataset, + mode="reset", + params={ + "base_path": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/GraspSampling/Assemblies", + "fixed_asset_cfg": SceneEntityCfg("insertive_object"), + "robot_ik_cfg": SceneEntityCfg( + "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" + ), + "gripper_cfg": SceneEntityCfg("robot", joint_names=["finger_joint", ".*right.*", ".*left.*"]), + "pose_range_b": { + "x": (-0.02, 0.02), + "y": (-0.02, 0.02), + "z": (-0.02, 0.02), + "roll": (-np.pi / 16, np.pi / 16), + "pitch": (-np.pi / 16, np.pi / 16), + "yaw": (-np.pi / 16, np.pi / 16), + }, + }, + ) + + +@configclass +class ObjectAnywhereEEGraspedEventCfg(ResetStatesBaseEventCfg): + reset_insertive_object_pose = EventTerm( + func=task_mdp.reset_root_states_uniform, + mode="reset", + params={ + "pose_range": { + "x": (0.3, 0.55), + "y": (-0.1, 0.3), + "z": (0.0, 0.3), + "roll": (-np.pi, np.pi), + "pitch": (-np.pi, np.pi), + "yaw": (-np.pi, np.pi), + }, + "velocity_range": {}, + "asset_cfgs": {"insertive_object": SceneEntityCfg("insertive_object")}, + "offset_asset_cfg": SceneEntityCfg("ur5_metal_support"), + "use_bottom_offset": True, + }, + ) + + reset_end_effector_pose_from_grasp_dataset = EventTerm( + func=task_mdp.reset_end_effector_from_grasp_dataset, + mode="reset", + params={ + "base_path": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/GraspSampling/Assemblies", + "fixed_asset_cfg": SceneEntityCfg("insertive_object"), + "robot_ik_cfg": SceneEntityCfg( + "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" + ), + "gripper_cfg": SceneEntityCfg("robot", joint_names=["finger_joint", ".*right.*", ".*left.*"]), + "pose_range_b": { + "x": (0.0, 0.0), + "y": (0.0, 0.0), + "z": (0.0, 0.0), + "roll": (0.0, 0.0), + "pitch": (0.0, 0.0), + "yaw": (0.0, 0.0), + }, + }, + ) + + +@configclass +class ObjectPartiallyAssembledEEAnywhereEventCfg(ResetStatesBaseEventCfg): + reset_insertive_object_pose_from_partial_assembly_dataset = EventTerm( + func=task_mdp.reset_insertive_object_from_partial_assembly_dataset, + mode="reset", + params={ + "base_path": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/PartialAssemblies/Assemblies", + "insertive_object_cfg": SceneEntityCfg("insertive_object"), + "receptive_object_cfg": SceneEntityCfg("receptive_object"), + "pose_range_b": { + "x": (0.0, 0.0), + "y": (0.0, 0.0), + "z": (0.0, 0.0), + "roll": (0.0, 0.0), + "pitch": (0.0, 0.0), + "yaw": (0.0, 0.0), + }, + }, + ) + + reset_end_effector_pose = EventTerm( + func=task_mdp.reset_end_effector_round_fixed_asset, + mode="reset", + params={ + "fixed_asset_cfg": SceneEntityCfg("robot"), + "fixed_asset_offset": None, + "pose_range_b": { + "x": (0.3, 0.7), + "y": (-0.4, 0.4), + "z": (0.5, 0.5), + "roll": (0.0, 0.0), + "pitch": (np.pi / 4, 3 * np.pi / 4), + "yaw": (np.pi / 2, 3 * np.pi / 2), + }, + "robot_ik_cfg": SceneEntityCfg( + "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" + ), + }, + ) + + +@configclass +class ObjectPartiallyAssembledEEGraspedEventCfg(ResetStatesBaseEventCfg): + reset_insertive_object_pose_from_partial_assembly_dataset = EventTerm( + func=task_mdp.reset_insertive_object_from_partial_assembly_dataset, + mode="reset", + params={ + "base_path": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/PartialAssemblies/Assemblies", + "insertive_object_cfg": SceneEntityCfg("insertive_object"), + "receptive_object_cfg": SceneEntityCfg("receptive_object"), + "pose_range_b": { + "x": (0.0, 0.0), + "y": (0.0, 0.0), + "z": (0.0, 0.0), + "roll": (0.0, 0.0), + "pitch": (0.0, 0.0), + "yaw": (0.0, 0.0), + }, + }, + ) + + reset_end_effector_pose_from_grasp_dataset = EventTerm( + func=task_mdp.reset_end_effector_from_grasp_dataset, + mode="reset", + params={ + "base_path": f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/GraspSampling/Assemblies", + "fixed_asset_cfg": SceneEntityCfg("insertive_object"), + "robot_ik_cfg": SceneEntityCfg( + "robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"], body_names="robotiq_base_link" + ), + "gripper_cfg": SceneEntityCfg("robot", joint_names=["finger_joint", ".*right.*", ".*left.*"]), + "pose_range_b": { + "x": (-0.01, 0.01), + "y": (-0.01, 0.01), + "z": (-0.01, 0.01), + "roll": (-np.pi / 32, np.pi / 32), + "pitch": (-np.pi / 32, np.pi / 32), + "yaw": (-np.pi / 32, np.pi / 32), + }, + }, + ) + + +@configclass +class ResetStatesTerminationCfg: + """Configuration for reset states termination conditions.""" + + time_out = DoneTerm(func=task_mdp.time_out, time_out=True) + + abnormal_robot = DoneTerm(func=task_mdp.abnormal_robot_state) + + success = DoneTerm( + func=task_mdp.check_reset_state_success, + params={ + "object_cfgs": [SceneEntityCfg("insertive_object"), SceneEntityCfg("receptive_object")], + "robot_cfg": SceneEntityCfg("robot"), + "ee_body_name": "robotiq_base_link", + "collision_analyzer_cfgs": [ + task_mdp.CollisionAnalyzerCfg( + num_points=1024, + max_dist=0.5, + min_dist=-0.0005, + asset_cfg=SceneEntityCfg("robot"), + obstacle_cfgs=[SceneEntityCfg("insertive_object")], + ), + task_mdp.CollisionAnalyzerCfg( + num_points=1024, + max_dist=0.5, + min_dist=0.0, + asset_cfg=SceneEntityCfg("robot"), + obstacle_cfgs=[SceneEntityCfg("receptive_object")], + ), + task_mdp.CollisionAnalyzerCfg( + num_points=1024, + max_dist=0.5, + min_dist=-0.0005, + asset_cfg=SceneEntityCfg("insertive_object"), + obstacle_cfgs=[SceneEntityCfg("receptive_object")], + ), + ], + "max_robot_pos_deviation": 0.05, + "max_object_pos_deviation": MISSING, + "pos_z_threshold": -0.01, + "consecutive_stability_steps": 5, + }, + time_out=True, + ) + + +@configclass +class ResetStatesObservationsCfg: + """Configuration for reset states observations.""" + + pass + + +@configclass +class ResetStatesRewardsCfg: + """Configuration for reset states rewards.""" + + pass + + +def make_insertive_object(usd_path: str): + return RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/InsertiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=usd_path, + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=False, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +def make_receptive_object(usd_path: str): + return RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ReceptiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=usd_path, + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=True, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +variants = { + "scene.insertive_object": { + "fbleg": make_insertive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/SquareLeg/square_leg.usd"), + "fbdrawerbottom": make_insertive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/DrawerBottom/drawer_bottom.usd" + ), + "peg": make_insertive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd"), + }, + "scene.receptive_object": { + "fbtabletop": make_receptive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/SquareTableTop/square_table_top.usd" + ), + "fbdrawerbox": make_receptive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/DrawerBox/drawer_box.usd" + ), + "peghole": make_receptive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/PegHole/peg_hole.usd"), + }, +} + + +@configclass +class UR5eRobotiq2f85ResetStatesCfg(ManagerBasedRLEnvCfg): + """Configuration for reset states environment with UR5e Robotiq 2F85 gripper.""" + + scene: ResetStatesSceneCfg = ResetStatesSceneCfg(num_envs=1, env_spacing=1.5) + events: ResetStatesBaseEventCfg = MISSING + terminations: ResetStatesTerminationCfg = ResetStatesTerminationCfg() + observations: ResetStatesObservationsCfg = ResetStatesObservationsCfg() + actions: Ur5eRobotiq2f85RelativeOSCAction = Ur5eRobotiq2f85RelativeOSCAction() + rewards: ResetStatesRewardsCfg = ResetStatesRewardsCfg() + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 0.0, 0.75), origin_type="world", env_index=0, asset_name="robot") + variants = variants + + def __post_init__(self): + self.decimation = 12 + self.episode_length_s = 2.0 + # simulation settings + self.sim.dt = 1 / 120.0 + + # Contact and solver settings + self.sim.physx.solver_type = 1 + self.sim.physx.max_position_iteration_count = 192 + self.sim.physx.max_velocity_iteration_count = 1 + self.sim.physx.bounce_threshold_velocity = 0.02 + self.sim.physx.friction_offset_threshold = 0.01 + self.sim.physx.friction_correlation_distance = 0.0005 + + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 + self.sim.physx.gpu_max_rigid_contact_count = 2**23 + self.sim.physx.gpu_max_rigid_patch_count = 2**23 + self.sim.physx.gpu_collision_stack_size = 2**31 + + # Render settings + self.sim.render.enable_dlssg = True + self.sim.render.enable_ambient_occlusion = True + self.sim.render.enable_reflections = True + self.sim.render.enable_dl_denoiser = True + + +@configclass +class ObjectAnywhereEEAnywhereResetStatesCfg(UR5eRobotiq2f85ResetStatesCfg): + events: ObjectAnywhereEEAnywhereEventCfg = ObjectAnywhereEEAnywhereEventCfg() + + def __post_init__(self): + super().__post_init__() + self.terminations.success.params["max_object_pos_deviation"] = np.inf + + +@configclass +class ObjectRestingEEGraspedResetStatesCfg(UR5eRobotiq2f85ResetStatesCfg): + events: ObjectRestingEEGraspedEventCfg = ObjectRestingEEGraspedEventCfg() + + def __post_init__(self): + super().__post_init__() + self.terminations.success.params["max_object_pos_deviation"] = 0.01 + + +@configclass +class ObjectAnywhereEEGraspedResetStatesCfg(UR5eRobotiq2f85ResetStatesCfg): + events: ObjectAnywhereEEGraspedEventCfg = ObjectAnywhereEEGraspedEventCfg() + + def __post_init__(self): + super().__post_init__() + self.terminations.success.params["max_object_pos_deviation"] = 0.05 + + +@configclass +class ObjectPartiallyAssembledEEAnywhereResetStatesCfg(UR5eRobotiq2f85ResetStatesCfg): + events: ObjectPartiallyAssembledEEAnywhereEventCfg = ObjectPartiallyAssembledEEAnywhereEventCfg() + + def __post_init__(self): + super().__post_init__() + self.terminations.success.params["max_object_pos_deviation"] = 0.005 + + +@configclass +class ObjectPartiallyAssembledEEGraspedResetStatesCfg(UR5eRobotiq2f85ResetStatesCfg): + events: ObjectPartiallyAssembledEEGraspedEventCfg = ObjectPartiallyAssembledEEGraspedEventCfg() + + def __post_init__(self): + super().__post_init__() + self.terminations.success.params["max_object_pos_deviation"] = 0.005 diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/rl_state_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/rl_state_cfg.py new file mode 100644 index 0000000..5acd0fa --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/config/ur5e_robotiq_2f85/rl_state_cfg.py @@ -0,0 +1,705 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +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 uwlab_assets import UWLAB_CLOUD_ASSETS_DIR +from uwlab_assets.robots.ur5e_robotiq_gripper import ( + EXPLICIT_UR5E_ROBOTIQ_2F85, + IMPLICIT_UR5E_ROBOTIQ_2F85, + Ur5eRobotiq2f85RelativeJointPositionAction, +) + +from uwlab_tasks.manager_based.manipulation.reset_states.config.ur5e_robotiq_2f85.actions import ( + Ur5eRobotiq2f85RelativeOSCAction, +) + +from ... import mdp as task_mdp + + +@configclass +class RlStateSceneCfg(InteractiveSceneCfg): + """Scene configuration for RL state environment.""" + + robot = EXPLICIT_UR5E_ROBOTIQ_2F85.replace(prim_path="{ENV_REGEX_NS}/Robot") + + insertive_object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/InsertiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd", + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=False, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.02), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + receptive_object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ReceptiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/PegHole/peg_hole.usd", + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + # receptive object does not move + kinematic_enabled=True, + ), + # since kinematic_enabled=True, mass does not matter + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + # Environment + table = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Table", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.4, 0.0, -0.881), rot=(0.707, 0.0, 0.0, -0.707)), + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention/pat_vention.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), + ), + ) + + ur5_metal_support = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/UR5MetalSupport", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0, -0.013), rot=(1.0, 0.0, 0.0, 0.0)), + spawn=sim_utils.UsdFileCfg( + usd_path=f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Mounts/UWPatVention2/Ur5MetalSupport/ur5plate.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), + ), + ) + + ground = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, -0.868)), + spawn=sim_utils.GroundPlaneCfg(), + ) + + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=10000.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + +@configclass +class BaseEventCfg: + """Configuration for events.""" + + # mode: startup (randomize dynamics) + robot_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "static_friction_range": (0.3, 1.2), + "dynamic_friction_range": (0.2, 1.0), + "restitution_range": (0.0, 0.0), + "num_buckets": 256, + "asset_cfg": SceneEntityCfg("robot"), + "make_consistent": True, + }, + ) + + # use large friction to avoid slipping + insertive_object_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "static_friction_range": (1.0, 2.0), + "dynamic_friction_range": (0.9, 1.9), + "restitution_range": (0.0, 0.0), + "num_buckets": 256, + "asset_cfg": SceneEntityCfg("insertive_object"), + "make_consistent": True, + }, + ) + + # use large friction to avoid slipping + receptive_object_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "static_friction_range": (1.0, 2.0), + "dynamic_friction_range": (0.9, 1.9), + "restitution_range": (0.0, 0.0), + "num_buckets": 256, + "asset_cfg": SceneEntityCfg("receptive_object"), + "make_consistent": True, + }, + ) + + table_material = EventTerm( + func=task_mdp.randomize_rigid_body_material, # type: ignore + mode="startup", + params={ + "static_friction_range": (0.3, 0.6), + "dynamic_friction_range": (0.2, 0.5), + "restitution_range": (0.0, 0.0), + "num_buckets": 256, + "asset_cfg": SceneEntityCfg("table"), + "make_consistent": True, + }, + ) + + randomize_robot_mass = EventTerm( + func=task_mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot"), + "mass_distribution_params": (0.7, 1.3), + "operation": "scale", + "distribution": "uniform", + "recompute_inertia": True, + }, + ) + + randomize_insertive_object_mass = EventTerm( + func=task_mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("insertive_object"), + # we assume insertive object is somewhere between 20g and 200g + "mass_distribution_params": (0.02, 0.2), + "operation": "abs", + "distribution": "uniform", + "recompute_inertia": True, + }, + ) + + randomize_receptive_object_mass = EventTerm( + func=task_mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("receptive_object"), + "mass_distribution_params": (0.5, 1.5), + "operation": "scale", + "distribution": "uniform", + "recompute_inertia": True, + }, + ) + + randomize_table_mass = EventTerm( + func=task_mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("table"), + "mass_distribution_params": (0.5, 1.5), + "operation": "scale", + "distribution": "uniform", + "recompute_inertia": True, + }, + ) + + randomize_robot_joint_parameters = EventTerm( + func=task_mdp.randomize_joint_parameters, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder.*", "elbow.*", "wrist.*", "finger_joint"]), + "friction_distribution_params": (0.25, 4.0), + "armature_distribution_params": (0.25, 4.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + + randomize_gripper_actuator_parameters = EventTerm( + func=task_mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["finger_joint"]), + "stiffness_distribution_params": (0.5, 2.0), + "damping_distribution_params": (0.5, 2.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + + # mode: reset + reset_everything = EventTerm(func=task_mdp.reset_scene_to_default, mode="reset", params={}) + + +@configclass +class TrainEventCfg(BaseEventCfg): + """Configuration for training events.""" + + reset_from_reset_states = EventTerm( + func=task_mdp.MultiResetManager, + mode="reset", + params={ + "base_paths": [ + f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/Resets/Assemblies/ObjectAnywhereEEAnywhere", + f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/Resets/Assemblies/ObjectRestingEEGrasped", + f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/Resets/Assemblies/ObjectAnywhereEEGrasped", + f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/Resets/Assemblies/ObjectPartiallyAssembledEEGrasped", + ], + "probs": [0.25, 0.25, 0.25, 0.25], + "success": "env.reward_manager.get_term_cfg('progress_context').func.success", + }, + ) + + +@configclass +class EvalEventCfg(BaseEventCfg): + """Configuration for evaluation events.""" + + reset_from_reset_states = EventTerm( + func=task_mdp.MultiResetManager, + mode="reset", + params={ + "base_paths": [ + f"{UWLAB_CLOUD_ASSETS_DIR}/Datasets/Resets/Assemblies/ObjectAnywhereEEAnywhere", + ], + "probs": [1.0], + "success": "env.reward_manager.get_term_cfg('progress_context').func.success", + }, + ) + + +@configclass +class CommandsCfg: + """Command specifications for the MDP.""" + + task_command = task_mdp.TaskCommandCfg( + asset_cfg=SceneEntityCfg("robot", body_names="body"), + resampling_time_range=(1e6, 1e6), + success_position_threshold=0.0025, + success_orientation_threshold=0.025, + insertive_asset_cfg=SceneEntityCfg("insertive_object"), + receptive_asset_cfg=SceneEntityCfg("receptive_object"), + ) + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + prev_actions = ObsTerm(func=task_mdp.last_action) + + joint_pos = ObsTerm(func=task_mdp.joint_pos) + + end_effector_pose = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame_with_metadata, + params={ + "target_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "root_asset_cfg": SceneEntityCfg("robot"), + "target_asset_offset_metadata_key": "gripper_offset", + "root_asset_offset_metadata_key": "offset", + "rotation_repr": "axis_angle", + }, + ) + + insertive_asset_pose = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame_with_metadata, + params={ + "target_asset_cfg": SceneEntityCfg("insertive_object"), + "root_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "root_asset_offset_metadata_key": "gripper_offset", + "rotation_repr": "axis_angle", + }, + ) + + receptive_asset_pose = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame, + params={ + "target_asset_cfg": SceneEntityCfg("receptive_object"), + "root_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "rotation_repr": "axis_angle", + }, + ) + + insertive_asset_in_receptive_asset_frame: ObsTerm = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame, + params={ + "target_asset_cfg": SceneEntityCfg("insertive_object"), + "root_asset_cfg": SceneEntityCfg("receptive_object"), + "rotation_repr": "axis_angle", + }, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + self.history_length = 5 + + @configclass + class CriticCfg(ObsGroup): + """Critic observations for policy group.""" + + prev_actions = ObsTerm(func=task_mdp.last_action) + + joint_pos = ObsTerm(func=task_mdp.joint_pos) + + end_effector_pose = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame_with_metadata, + params={ + "target_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "root_asset_cfg": SceneEntityCfg("robot"), + "target_asset_offset_metadata_key": "gripper_offset", + "root_asset_offset_metadata_key": "offset", + "rotation_repr": "axis_angle", + }, + ) + + insertive_asset_pose = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame_with_metadata, + params={ + "target_asset_cfg": SceneEntityCfg("insertive_object"), + "root_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "root_asset_offset_metadata_key": "gripper_offset", + "rotation_repr": "axis_angle", + }, + ) + + receptive_asset_pose = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame, + params={ + "target_asset_cfg": SceneEntityCfg("receptive_object"), + "root_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "rotation_repr": "axis_angle", + }, + ) + + insertive_asset_in_receptive_asset_frame: ObsTerm = ObsTerm( + func=task_mdp.target_asset_pose_in_root_asset_frame, + params={ + "target_asset_cfg": SceneEntityCfg("insertive_object"), + "root_asset_cfg": SceneEntityCfg("receptive_object"), + "rotation_repr": "axis_angle", + }, + ) + + # privileged observations + time_left = ObsTerm(func=task_mdp.time_left) + + joint_vel = ObsTerm(func=task_mdp.joint_vel) + + end_effector_vel_lin_ang_b = ObsTerm( + func=task_mdp.asset_link_velocity_in_root_asset_frame, + params={ + "target_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "root_asset_cfg": SceneEntityCfg("robot"), + }, + ) + + robot_material_properties = ObsTerm( + func=task_mdp.get_material_properties, params={"asset_cfg": SceneEntityCfg("robot")} + ) + + insertive_object_material_properties = ObsTerm( + func=task_mdp.get_material_properties, params={"asset_cfg": SceneEntityCfg("insertive_object")} + ) + + receptive_object_material_properties = ObsTerm( + func=task_mdp.get_material_properties, params={"asset_cfg": SceneEntityCfg("receptive_object")} + ) + + table_material_properties = ObsTerm( + func=task_mdp.get_material_properties, params={"asset_cfg": SceneEntityCfg("table")} + ) + + robot_mass = ObsTerm(func=task_mdp.get_mass, params={"asset_cfg": SceneEntityCfg("robot")}) + + insertive_object_mass = ObsTerm( + func=task_mdp.get_mass, params={"asset_cfg": SceneEntityCfg("insertive_object")} + ) + + receptive_object_mass = ObsTerm( + func=task_mdp.get_mass, params={"asset_cfg": SceneEntityCfg("receptive_object")} + ) + + table_mass = ObsTerm(func=task_mdp.get_mass, params={"asset_cfg": SceneEntityCfg("table")}) + + robot_joint_friction = ObsTerm(func=task_mdp.get_joint_friction, params={"asset_cfg": SceneEntityCfg("robot")}) + + robot_joint_armature = ObsTerm(func=task_mdp.get_joint_armature, params={"asset_cfg": SceneEntityCfg("robot")}) + + robot_joint_stiffness = ObsTerm( + func=task_mdp.get_joint_stiffness, params={"asset_cfg": SceneEntityCfg("robot")} + ) + + robot_joint_damping = ObsTerm(func=task_mdp.get_joint_damping, params={"asset_cfg": SceneEntityCfg("robot")}) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + self.history_length = 1 + + # observation groups + policy: PolicyCfg = PolicyCfg() + critic: CriticCfg = CriticCfg() + + +@configclass +class RewardsCfg: + + # safety rewards + + action_magnitude = RewTerm(func=task_mdp.action_l2_clamped, weight=-1e-4) + + action_rate = RewTerm(func=task_mdp.action_rate_l2_clamped, weight=-1e-4) + + joint_vel = RewTerm( + func=task_mdp.joint_vel_l2_clamped, + weight=-1e-3, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder.*", "elbow.*", "wrist.*"])}, + ) + + abnormal_robot = RewTerm(func=task_mdp.abnormal_robot_state, weight=-100.0) + + # task rewards + + progress_context = RewTerm( + func=task_mdp.ProgressContext, # type: ignore + weight=0.1, + params={ + "insertive_asset_cfg": SceneEntityCfg("insertive_object"), + "receptive_asset_cfg": SceneEntityCfg("receptive_object"), + }, + ) + + ee_asset_distance = RewTerm( + func=task_mdp.ee_asset_distance_tanh, + weight=0.1, + params={ + "root_asset_cfg": SceneEntityCfg("robot", body_names="robotiq_base_link"), + "target_asset_cfg": SceneEntityCfg("insertive_object"), + "root_asset_offset_metadata_key": "gripper_offset", + "std": 1.0, + }, + ) + + dense_success_reward = RewTerm(func=task_mdp.dense_success_reward, weight=0.1, params={"std": 1.0}) + + success_reward = RewTerm(func=task_mdp.success_reward, weight=1.0) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=task_mdp.time_out, time_out=True) + + abnormal_robot = DoneTerm(func=task_mdp.abnormal_robot_state) + + +def make_insertive_object(usd_path: str): + return RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/InsertiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=usd_path, + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=False, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.001), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +def make_receptive_object(usd_path: str): + return RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ReceptiveObject", + spawn=sim_utils.UsdFileCfg( + usd_path=usd_path, + scale=(1, 1, 1), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + disable_gravity=False, + kinematic_enabled=True, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.5), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + +variants = { + "scene.insertive_object": { + "fbleg": make_insertive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/SquareLeg/square_leg.usd"), + "fbdrawerbottom": make_insertive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/DrawerBottom/drawer_bottom.usd" + ), + "peg": make_insertive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/Peg/peg.usd"), + }, + "scene.receptive_object": { + "fbtabletop": make_receptive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/SquareTableTop/square_table_top.usd" + ), + "fbdrawerbox": make_receptive_object( + f"{UWLAB_CLOUD_ASSETS_DIR}/Props/FurnitureBench/DrawerBox/drawer_box.usd" + ), + "peghole": make_receptive_object(f"{UWLAB_CLOUD_ASSETS_DIR}/Props/Custom/PegHole/peg_hole.usd"), + }, +} + + +@configclass +class Ur5eRobotiq2f85RlStateCfg(ManagerBasedRLEnvCfg): + scene: RlStateSceneCfg = RlStateSceneCfg(num_envs=32, env_spacing=1.5) + observations: ObservationsCfg = ObservationsCfg() + actions: Ur5eRobotiq2f85RelativeOSCAction = Ur5eRobotiq2f85RelativeOSCAction() + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: BaseEventCfg = MISSING + commands: CommandsCfg = CommandsCfg() + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 0.0, 0.75), origin_type="world", env_index=0, asset_name="robot") + variants = variants + + def __post_init__(self): + self.decimation = 12 + self.episode_length_s = 16.0 + # simulation settings + self.sim.dt = 1 / 120.0 + + # Contact and solver settings + self.sim.physx.solver_type = 1 + self.sim.physx.max_position_iteration_count = 192 + self.sim.physx.max_velocity_iteration_count = 1 + self.sim.physx.bounce_threshold_velocity = 0.02 + self.sim.physx.friction_offset_threshold = 0.01 + self.sim.physx.friction_correlation_distance = 0.0005 + + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 2**23 + self.sim.physx.gpu_max_rigid_contact_count = 2**23 + self.sim.physx.gpu_max_rigid_patch_count = 2**23 + self.sim.physx.gpu_collision_stack_size = 2**31 + + # Render settings + self.sim.render.enable_dlssg = True + self.sim.render.enable_ambient_occlusion = True + self.sim.render.enable_reflections = True + self.sim.render.enable_dl_denoiser = True + + +# Training configurations +@configclass +class Ur5eRobotiq2f85RelCartesianOSCTrainCfg(Ur5eRobotiq2f85RlStateCfg): + """Training configuration for Relative Cartesian OSC action space.""" + + events: TrainEventCfg = TrainEventCfg() + actions: Ur5eRobotiq2f85RelativeOSCAction = Ur5eRobotiq2f85RelativeOSCAction() + + def __post_init__(self): + super().__post_init__() + self.scene.robot = EXPLICIT_UR5E_ROBOTIQ_2F85.replace(prim_path="{ENV_REGEX_NS}/Robot") + + self.events.randomize_robot_actuator_parameters = EventTerm( + func=task_mdp.randomize_operational_space_controller_gains, + mode="reset", + params={ + "action_name": "arm", + "stiffness_distribution_params": (0.7, 1.3), + "damping_distribution_params": (0.9, 1.1), + "operation": "scale", + "distribution": "uniform", + }, + ) + + +@configclass +class Ur5eRobotiq2f85RelJointPosTrainCfg(Ur5eRobotiq2f85RlStateCfg): + """Training configuration for Relative Joint Position action space.""" + + events: TrainEventCfg = TrainEventCfg() + actions: Ur5eRobotiq2f85RelativeJointPositionAction = Ur5eRobotiq2f85RelativeJointPositionAction() + + def __post_init__(self): + super().__post_init__() + self.scene.robot = IMPLICIT_UR5E_ROBOTIQ_2F85.replace(prim_path="{ENV_REGEX_NS}/Robot") + + self.events.randomize_robot_actuator_parameters = EventTerm( + func=task_mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder.*", "elbow.*", "wrist.*", "finger_joint"]), + "stiffness_distribution_params": (0.5, 2.0), + "damping_distribution_params": (0.5, 2.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + + +# Evaluation configurations +@configclass +class Ur5eRobotiq2f85RelCartesianOSCEvalCfg(Ur5eRobotiq2f85RlStateCfg): + """Evaluation configuration for Relative Cartesian OSC action space.""" + + events: EvalEventCfg = EvalEventCfg() + actions: Ur5eRobotiq2f85RelativeOSCAction = Ur5eRobotiq2f85RelativeOSCAction() + + def __post_init__(self): + super().__post_init__() + self.scene.robot = EXPLICIT_UR5E_ROBOTIQ_2F85.replace(prim_path="{ENV_REGEX_NS}/Robot") + + self.events.randomize_robot_actuator_parameters = EventTerm( + func=task_mdp.randomize_operational_space_controller_gains, + mode="reset", + params={ + "action_name": "arm", + "stiffness_distribution_params": (0.7, 1.3), + "damping_distribution_params": (0.9, 1.1), + "operation": "scale", + "distribution": "uniform", + }, + ) + + +@configclass +class Ur5eRobotiq2f85RelJointPosEvalCfg(Ur5eRobotiq2f85RlStateCfg): + """Evaluation configuration for Relative Joint Position action space.""" + + events: EvalEventCfg = EvalEventCfg() + actions: Ur5eRobotiq2f85RelativeJointPositionAction = Ur5eRobotiq2f85RelativeJointPositionAction() + + def __post_init__(self): + super().__post_init__() + self.scene.robot = IMPLICIT_UR5E_ROBOTIQ_2F85.replace(prim_path="{ENV_REGEX_NS}/Robot") + + self.events.randomize_robot_actuator_parameters = EventTerm( + func=task_mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=["shoulder.*", "elbow.*", "wrist.*", "finger_joint"]), + "stiffness_distribution_params": (0.5, 2.0), + "damping_distribution_params": (0.5, 2.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/__init__.py new file mode 100644 index 0000000..f387958 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.envs.mdp import * + +from uwlab.envs.mdp import * + +from .commands_cfg import * +from .events import * +from .observations import * +from .recorders import * +from .rewards import * +from .terminations import * +from .utils import * diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/__init__.py new file mode 100644 index 0000000..ffee31f --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from .actions_cfg import * +from .task_space_actions import * diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/actions_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/actions_cfg.py new file mode 100644 index 0000000..8aa6827 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/actions_cfg.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from dataclasses import MISSING + +from isaaclab.envs.mdp.actions.actions_cfg import OperationalSpaceControllerActionCfg +from isaaclab.managers.action_manager import ActionTerm +from isaaclab.utils import configclass + +from . import task_space_actions + + +@configclass +class TransformedOperationalSpaceControllerActionCfg(OperationalSpaceControllerActionCfg): + """Configuration for Scaled Operational Space Controller action term. + + This action term uses the OperationalSpaceController directly and applies fixed scaling + to the input actions. The scaling values are applied per DOF (x, y, z, rx, ry, rz). + """ + + class_type: type[ActionTerm] = task_space_actions.TransformedOperationalSpaceControllerAction + + action_root_offset: OperationalSpaceControllerActionCfg.OffsetCfg | None = None + """Offset for the action root frame.""" + + scale_xyz_axisangle: tuple[float, float, float, float, float, float] = MISSING + """Fixed scaling values for [x, y, z, rx, ry, rz] where rotation is in axis-angle representation.""" + + input_clip: tuple[float, float] | None = None + """Input clip values for the action.""" diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/task_space_actions.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/task_space_actions.py new file mode 100644 index 0000000..2785cef --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/actions/task_space_actions.py @@ -0,0 +1,96 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +import isaaclab.utils.math as math_utils +from isaaclab.envs.mdp.actions.task_space_actions import OperationalSpaceControllerAction + +from . import actions_cfg + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + + +class TransformedOperationalSpaceControllerAction(OperationalSpaceControllerAction): + """Scaled Operational Space Controller action term. + + This action term inherits from OperationalSpaceControllerAction and applies fixed scaling + to the input actions. The scaling values are applied per DOF (x, y, z, rx, ry, rz) where + rotation is in axis-angle representation. + + The workflow is: + 1. Receive 6-DOF Cartesian commands [x, y, z, rx, ry, rz] (rotation in axis-angle) + 2. Apply fixed scaling per DOF + 3. Use parent OperationalSpaceControllerAction to handle the rest + """ + + cfg: actions_cfg.TransformedOperationalSpaceControllerActionCfg + """The configuration of the action term.""" + + def __init__(self, cfg: actions_cfg.TransformedOperationalSpaceControllerActionCfg, env: ManagerBasedEnv): + # Initialize the parent OSC action + super().__init__(cfg, env) + + self._scale = torch.tensor(cfg.scale_xyz_axisangle, device=self.device) + if cfg.input_clip is not None: + self._input_clip = torch.tensor(cfg.input_clip, device=self.device) + else: + self._input_clip = None + if self.cfg.action_root_offset is not None: + self._action_root_offset_pos = torch.tensor(cfg.action_root_offset.pos, device=self.device).repeat( + self.num_envs, 1 + ) + self._action_root_offset_quat = torch.tensor(cfg.action_root_offset.rot, device=self.device).repeat( + self.num_envs, 1 + ) + else: + self._action_root_offset_pos = None + self._action_root_offset_quat = None + + self._transformed_actions = torch.zeros_like(self.raw_actions) + + def process_actions(self, actions: torch.Tensor): + """Process actions by applying fixed scaling per DOF and coordinate frame transformation, then call parent method.""" + # Step 1: Apply scaling + scaled_actions_offset_coords = actions * self._scale + if self._input_clip is not None: + scaled_actions_offset_coords = torch.clamp( + scaled_actions_offset_coords, min=self._input_clip[0], max=self._input_clip[1] + ) + + self._transformed_actions[:] = scaled_actions_offset_coords + + if self._action_root_offset_pos is not None and self._action_root_offset_quat is not None: + # Step 2: Transform coordinate frame from offset-robot-base to standard-robot-base + # Extract position and rotation deltas + delta_pos_offset = scaled_actions_offset_coords[:, :3] # [x, y, z] + delta_rot_offset = scaled_actions_offset_coords[:, 3:6] # [rx, ry, rz] in axis-angle + + # Get rotation matrix from offset-robot-base to standard-robot-base + # The action_root_offset defines standard -> offset, so we need the inverse + R_offset_to_standard = math_utils.matrix_from_quat(math_utils.quat_inv(self._action_root_offset_quat)) + + # Transform position delta: rotate from offset coordinates to standard coordinates + delta_pos_standard = torch.bmm(R_offset_to_standard, delta_pos_offset.unsqueeze(-1)).squeeze(-1) + + # Transform rotation delta (axis-angle): rotate the axis from offset coordinates to standard coordinates + delta_rot_standard = torch.bmm(R_offset_to_standard, delta_rot_offset.unsqueeze(-1)).squeeze(-1) + + # Combine back into 6-DOF command + scaled_actions_standard_coords = torch.cat([delta_pos_standard, delta_rot_standard], dim=-1) + else: + scaled_actions_standard_coords = scaled_actions_offset_coords + + # Call parent process_actions with transformed actions + super().process_actions(scaled_actions_standard_coords) + + @property + def transformed_actions(self) -> torch.Tensor: + """Processed actions for operational space control.""" + return self._transformed_actions diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/collision_analyzer.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/collision_analyzer.py new file mode 100644 index 0000000..e19e886 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/collision_analyzer.py @@ -0,0 +1,194 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from torch.nn.utils.rnn import pad_sequence +from typing import TYPE_CHECKING + +import isaaclab.utils.math as math_utils +import warp as wp +from isaaclab.assets import RigidObject +from isaaclab.sim.utils import get_first_matching_child_prim +from pxr import UsdPhysics + +from . import utils +from .rigid_object_hasher import RigidObjectHasher + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + from .collision_analyzer_cfg import CollisionAnalyzerCfg + +# from isaaclab.markers import VisualizationMarkers +# from isaaclab.markers.config import RAY_CASTER_MARKER_CFG + +# ray_cfg = RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/ObservationPointCloudDebug") +# ray_cfg.markers["hit"].radius = 0.005 +# visualizer = VisualizationMarkers(ray_cfg) + + +class CollisionAnalyzer: + + cfg: CollisionAnalyzerCfg + + def __init__(self, cfg: CollisionAnalyzerCfg, env: ManagerBasedRLEnv): + self.cfg = cfg + self.asset: RigidObject = env.scene[cfg.asset_cfg.name] + self.obstacles: list[RigidObject] = [env.scene[cfg.name] for cfg in cfg.obstacle_cfgs] + # we support passing in a Articulation(joint connected rigidbodies) as asset, but that requires we collect all + # body names user intended to generate collision checks + body_names = ( + self.asset.body_names + if cfg.asset_cfg.body_names is None + else [self.asset.body_names[i] for i in cfg.asset_cfg.body_ids] + ) + if isinstance(body_names, str): + body_names = [body_names] + + self.body_ids = [] + self.local_pts = [] + for i, body_name in enumerate(body_names): + # start = time.perf_counter() + prim = get_first_matching_child_prim( + self.asset.cfg.prim_path.replace(".*", "0", 1), # we use the 0th env prim as template + predicate=lambda p: p.GetName() == body_name and p.HasAPI(UsdPhysics.RigidBodyAPI), + ) + local_pts = utils.sample_object_point_cloud( + num_envs=env.num_envs, + num_points=cfg.num_points, + prim_path_pattern=str(prim.GetPath()).replace("env_0", "env_.*", 1), + device=env.device, + ) + if local_pts is not None: + self.local_pts.append(local_pts.view(env.num_envs, 1, cfg.num_points, 3)) + self.body_ids.append(self.asset.body_names.index(body_name)) + # pc_time = time.perf_counter() - start + self.local_pts = torch.cat(self.local_pts, dim=1) + self.body_ids = torch.tensor(self.body_ids, dtype=torch.int, device=env.device) + + self.num_coll_per_obstacle_per_env = torch.empty( + (len(self.obstacles), env.num_envs), dtype=torch.int32, device=env.device + ) + self.obstacle_root_scales = torch.empty( + (len(self.obstacles), env.num_envs, 3), dtype=torch.float, device=env.device + ) + env_handles = [[[] for _ in range(env.num_envs)] for _ in range(len(self.obstacles))] + obstacle_relative_transforms = [[[] for _ in range(env.num_envs)] for _ in range(len(self.obstacles))] + + for i, obstacle in enumerate(self.obstacles): + # start = time.perf_counter() + obs_h = RigidObjectHasher(env.num_envs, prim_path_pattern=obstacle.cfg.prim_path, device=env.device) + for prim, p_hash, rel_tf, eid in zip( + obs_h.collider_prims, + obs_h.collider_prim_hashes, + obs_h.collider_prim_relative_transforms, + obs_h.collider_prim_env_ids, + ): + # convert each USD prim β†’ Warp mesh... + obstacle_relative_transforms[i][eid].append(rel_tf) + if p_hash.item() in obs_h.get_warp_mesh_store(): + env_handles[i][eid].append(obs_h.get_warp_mesh_store()[p_hash.item()].id) + else: + wp_mesh = utils.prim_to_warp_mesh(prim, device=env.device, relative_to_world=False) + obs_h.get_warp_mesh_store()[p_hash.item()] = wp_mesh + env_handles[i][eid].append(wp_mesh.id) + + self.num_coll_per_obstacle_per_env[i] = torch.bincount(obs_h.collider_prim_env_ids) + self.obstacle_root_scales[i] = obs_h.root_prim_scales + # pc_time = time.perf_counter() - start + # print(f"Sampled {len(obs_h.collider_prims)} wp meshes at '{obstacle.cfg.prim_path}' in {pc_time:.3f}s") + self.max_prims = torch.max(self.num_coll_per_obstacle_per_env).item() + handle_list = [] + rel_transform = [] + for i in range(len(env_handles)): + handle_list.extend([torch.tensor(lst, dtype=torch.int64, device=env.device) for lst in env_handles[i]]) + rel_transform.extend([torch.cat((tf), dim=0) for tf in obstacle_relative_transforms[i]]) + self.handles_tensor = pad_sequence(handle_list, batch_first=True, padding_value=0).view( + len(self.obstacles), env.num_envs, -1 + ) + self.collider_rel_transform = pad_sequence(rel_transform, batch_first=True, padding_value=0).view( + len(self.obstacles), env.num_envs, -1, 10 + ) + + def __call__(self, env: ManagerBasedRLEnv, env_ids: torch.Tensor): + pos_w = ( + self.asset.data.body_link_pos_w[env_ids][:, self.body_ids] + .unsqueeze(2) + .expand(-1, -1, self.cfg.num_points, 3) + ) + quat_w = ( + self.asset.data.body_link_quat_w[env_ids][:, self.body_ids] + .unsqueeze(2) + .expand(-1, -1, self.cfg.num_points, 4) + ) + cloud = math_utils.quat_apply(quat_w, self.local_pts[env_ids]) + pos_w # omit scale 1 + + obstacles_pos_w = torch.cat( + [ + obstacle.data.root_pos_w[env_ids].view(-1, 1, 1, 3).expand(-1, -1, self.cfg.num_points, 3) + for obstacle in self.obstacles + ], + dim=0, + ) + obstacles_quat_w = torch.cat( + [ + obstacle.data.root_quat_w[env_ids].view(-1, 1, 1, 4).expand(-1, cloud.shape[1], self.cfg.num_points, 4) + for obstacle in self.obstacles + ], + dim=0, + ) + obstacles_scale_w = ( + self.obstacle_root_scales[:, env_ids].view(-1, 1, 1, 3).expand(-1, -1, self.cfg.num_points, 3) + ) + could_root = ( + math_utils.quat_apply_inverse( + obstacles_quat_w, (cloud.repeat(len(self.obstacles), 1, 1, 1)) - obstacles_pos_w + ) + / obstacles_scale_w + ) + + total_points = len(self.body_ids) * self.cfg.num_points * len(self.obstacles) + queries = wp.from_torch(could_root.reshape(-1, 3), dtype=wp.vec3) + handles = wp.from_torch( + self.handles_tensor[:, env_ids].view(-1), dtype=wp.uint64 + ) # (num_obstacles * len(env_ids) * max_prims,) + counts = wp.from_torch(self.num_coll_per_obstacle_per_env[:, env_ids].view(-1), dtype=wp.int32) + coll_rel_pos = wp.from_torch(self.collider_rel_transform[:, env_ids, :, :3].view(-1, 3), dtype=wp.vec3) + coll_rel_quat = wp.from_torch(self.collider_rel_transform[:, env_ids, :, 3:7].view(-1, 4), dtype=wp.quat) + coll_rel_scale = wp.from_torch(self.collider_rel_transform[:, env_ids, :, 7:10].view(-1, 3), dtype=wp.vec3) + sign_w = wp.zeros((len(env_ids) * total_points,), dtype=float, device=env.device) + wp.launch( + utils.get_signed_distance, + dim=len(env_ids) * total_points, + inputs=[ + queries, + handles, + counts, + coll_rel_pos, + coll_rel_quat, + coll_rel_scale, + float(self.cfg.max_dist), + self.cfg.min_dist != 0.0, + len(env_ids), + len(self.body_ids) * self.cfg.num_points, + self.max_prims, + ], + outputs=[sign_w], + device=env.device, + ) + signs = ( + wp.to_torch(sign_w) + .view(len(self.obstacles), len(env_ids), len(self.body_ids), self.cfg.num_points) + .amin(dim=0) + ) + # collision_points = cloud[(signs < 0.0)] + # for i in range(500): + # env.sim.render() + # visualizer.visualize(collision_points.view(-1, 3)) + + coll_free_mask = signs.amin(dim=(1, 2)) >= self.cfg.min_dist + return coll_free_mask diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/collision_analyzer_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/collision_analyzer_cfg.py new file mode 100644 index 0000000..cdf0e7b --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/collision_analyzer_cfg.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from dataclasses import MISSING + +from isaaclab.managers import SceneEntityCfg +from isaaclab.utils import configclass + +from .collision_analyzer import CollisionAnalyzer + + +@configclass +class CollisionAnalyzerCfg: + + class_type: type[CollisionAnalyzer] = CollisionAnalyzer + + num_points: int = 32 + + max_dist: float = 0.5 + + min_dist: float = 0.0 + + asset_cfg: SceneEntityCfg = MISSING + + obstacle_cfgs: list[SceneEntityCfg] = MISSING diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/commands.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/commands.py new file mode 100644 index 0000000..07abab7 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/commands.py @@ -0,0 +1,167 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module containing command generators for the 2D-pose for locomotion tasks.""" + +from __future__ import annotations + +import inspect +import torch +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import CommandTerm + +from ..assembly_keypoints import Offset +from . import utils + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + + from .commands_cfg import TaskCommandCfg, TaskDependentCommandCfg + + +class TaskDependentCommand(CommandTerm): + cfg: TaskDependentCommandCfg + + def __init__(self, cfg: TaskDependentCommandCfg, env: ManagerBasedEnv): + # initialize the base class + super().__init__(cfg, env) + + self.reset_terms_when_resample = cfg.reset_terms_when_resample + self.interval_reset_terms = [] + self.reset_terms = [] + self.ALL_INDICES = torch.arange(self.num_envs, device=self.device) + for name, term_cfg in self.reset_terms_when_resample.items(): + if not (term_cfg.mode == "reset" or term_cfg.mode == "interval"): + raise ValueError(f"Term '{name}' in 'reset_terms_when_resample' must have mode 'reset' or 'interval'") + if inspect.isclass(term_cfg.func): + term_cfg.func = term_cfg.func(cfg=term_cfg, env=self._env) + if term_cfg.mode == "reset": + self.reset_terms.append(term_cfg) + elif term_cfg.mode == "interval": + if term_cfg.interval_range_s != (0, 0): + raise ValueError( + "task dependent events term with interval mode current only supports range of (0, 0)" + ) + self.interval_reset_terms.append(term_cfg) + + def _resample_command(self, env_ids: Sequence[int]): + for term in self.reset_terms: + func = term.func + func(self._env, env_ids, **term.params) + for term in self.interval_reset_terms: + func = term.func + func.reset(env_ids) + + def _update_command(self): + for term in self.interval_reset_terms: + func = term.func + func(self._env, self.ALL_INDICES, **term.params) + + def get_event(self, event_term_name: str): + """Get the event term by name.""" + return self.reset_terms_when_resample.get(event_term_name).func + + +class TaskCommand(TaskDependentCommand): + """Command generator that generates pose commands based on the terrain. + + This command generator samples the position commands from the valid patches of the terrain. + The heading commands are either set to point towards the target or are sampled uniformly. + + It expects the terrain to have a valid flat patches under the key 'target'. + """ + + cfg: TaskCommandCfg + """Configuration for the command generator.""" + + def __init__(self, cfg: TaskCommandCfg, env: ManagerBasedEnv): + # initialize the base class + super().__init__(cfg, env) + + # obtain the terrain asset + self.insertive_asset: Articulation | RigidObject = env.scene[cfg.insertive_asset_cfg.name] + self.receptive_asset: Articulation | RigidObject = env.scene[cfg.receptive_asset_cfg.name] + insertive_meta = utils.read_metadata_from_usd_directory(self.insertive_asset.cfg.spawn.usd_path) + receptive_meta = utils.read_metadata_from_usd_directory(self.receptive_asset.cfg.spawn.usd_path) + self.insertive_asset_offset = Offset( + pos=tuple(insertive_meta.get("assembled_offset").get("pos")), + quat=tuple(insertive_meta.get("assembled_offset").get("quat")), + ) + self.receptive_asset_offset = Offset( + pos=tuple(receptive_meta.get("assembled_offset").get("pos")), + quat=tuple(receptive_meta.get("assembled_offset").get("quat")), + ) + self.success_position_threshold: float = cfg.success_position_threshold + self.success_orientation_threshold: float = cfg.success_orientation_threshold + + self.metrics["average_rot_align_error"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["average_pos_align_error"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["end_of_episode_rot_align_error"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["end_of_episode_pos_align_error"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["end_of_episode_success_rate"] = torch.zeros(self.num_envs, device=self.device) + + self.orientation_aligned = torch.zeros((self._env.num_envs), dtype=torch.bool, device=self._env.device) + self.position_aligned = torch.zeros((self._env.num_envs), dtype=torch.bool, device=self._env.device) + self.euler_xy_distance = torch.zeros((self._env.num_envs), device=self._env.device) + self.xyz_distance = torch.zeros((self._env.num_envs), device=self._env.device) + + """ + Properties + """ + + @property + def command(self) -> torch.Tensor: + return torch.zeros(self.num_envs, 3, device=self.device) + + """ + Implementation specific functions. + """ + + def _update_metrics(self): + # logs end of episode data + reset_env = self._env.episode_length_buf == 0 + self.metrics["end_of_episode_rot_align_error"][reset_env] = self.euler_xy_distance[reset_env] + self.metrics["end_of_episode_pos_align_error"][reset_env] = self.xyz_distance[reset_env] + last_episode_success = (self.orientation_aligned & self.position_aligned)[reset_env] + self.metrics["end_of_episode_success_rate"][reset_env] = last_episode_success.float() + + # logs current data + insertive_asset_alignment_pos_w, insertive_asset_alignment_quat_w = self.insertive_asset_offset.apply( + self.insertive_asset + ) + receptive_asset_alignment_pos_w, receptive_asset_alignment_quat_w = self.receptive_asset_offset.apply( + self.receptive_asset + ) + insertive_asset_in_receptive_asset_frame_pos, insertive_asset_in_receptive_asset_frame_quat = ( + math_utils.subtract_frame_transforms( + receptive_asset_alignment_pos_w, + receptive_asset_alignment_quat_w, + insertive_asset_alignment_pos_w, + insertive_asset_alignment_quat_w, + ) + ) + e_x, e_y, _ = math_utils.euler_xyz_from_quat(insertive_asset_in_receptive_asset_frame_quat) + self.euler_xy_distance[:] = math_utils.wrap_to_pi(e_x).abs() + math_utils.wrap_to_pi(e_y).abs() + self.xyz_distance[:] = torch.norm(insertive_asset_in_receptive_asset_frame_pos, dim=1) + self.position_aligned[:] = self.xyz_distance < self.success_position_threshold + self.orientation_aligned[:] = self.euler_xy_distance < self.success_orientation_threshold + self.metrics["average_rot_align_error"][:] = self.euler_xy_distance + self.metrics["average_pos_align_error"][:] = self.xyz_distance + + def _resample_command(self, env_ids: Sequence[int]): + super()._resample_command(env_ids) + + def _update_command(self): + super()._update_command() + + def _set_debug_vis_impl(self, debug_vis: bool): + pass + + def _debug_vis_callback(self, event): + pass diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/commands_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/commands_cfg.py new file mode 100644 index 0000000..3ff5e27 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/commands_cfg.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from isaaclab.managers import CommandTermCfg +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.utils import configclass + +from .commands import TaskCommand, TaskDependentCommand + + +@configclass +class TaskDependentCommandCfg(CommandTermCfg): + class_type: type = TaskDependentCommand + + reset_terms_when_resample: dict[str, EventTerm] = {} + + +@configclass +class TaskCommandCfg(TaskDependentCommandCfg): + class_type: type = TaskCommand + + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") + + success_position_threshold: float = MISSING + + success_orientation_threshold: float = MISSING + + insertive_asset_cfg: SceneEntityCfg = MISSING + + receptive_asset_cfg: SceneEntityCfg = MISSING diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/events.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/events.py new file mode 100644 index 0000000..50d27ba --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/events.py @@ -0,0 +1,1352 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Event functions for manipulation tasks.""" + +import numpy as np +import os +import scipy.stats as stats +import tempfile +import torch +import trimesh +import trimesh.transformations as tra + +import carb +import isaaclab.sim as sim_utils +import isaaclab.utils.math as math_utils +import omni.usd +from isaaclab.assets import Articulation, RigidObject +from isaaclab.controllers import DifferentialIKControllerCfg +from isaaclab.envs import ManagerBasedEnv +from isaaclab.envs.mdp.actions.task_space_actions import DifferentialInverseKinematicsAction +from isaaclab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg +from isaaclab.markers import VisualizationMarkers +from isaaclab.markers.config import FRAME_MARKER_CFG +from isaaclab.utils.assets import retrieve_file_path +from pxr import UsdGeom + +from uwlab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg + +from uwlab_tasks.manager_based.manipulation.reset_states.mdp import utils + +from ..assembly_keypoints import Offset +from .success_monitor_cfg import SuccessMonitorCfg + + +class grasp_sampling_event(ManagerTermBase): + """EventTerm class for grasp sampling and positioning gripper.""" + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + # Extract parameters from config + self.object_cfg = cfg.params.get("object_cfg") + self.gripper_cfg = cfg.params.get("gripper_cfg") + self.num_candidates = cfg.params.get("num_candidates") + self.num_standoff_samples = cfg.params.get("num_standoff_samples") + self.num_orientations = cfg.params.get("num_orientations") + self.lateral_sigma = cfg.params.get("lateral_sigma") + self.visualize_grasps = cfg.params.get("visualize_grasps", False) + self.visualization_scale = cfg.params.get("visualization_scale", 0.03) + + # Read parameters from object metadata + gripper_asset = env.scene[self.gripper_cfg.name] + usd_path = gripper_asset.cfg.spawn.usd_path + metadata = utils.read_metadata_from_usd_directory(usd_path) + + # Extract parameters from metadata + self.gripper_maximum_aperture = metadata.get("maximum_aperture") + self.finger_offset = metadata.get("finger_offset") + self.finger_clearance = metadata.get("finger_clearance") + self.gripper_approach_direction = tuple(metadata.get("gripper_approach_direction")) + self.grasp_align_axis = tuple(metadata.get("grasp_align_axis")) + self.orientation_sample_axis = tuple(metadata.get("orientation_sample_axis")) + self.gripper_joint_reset_config = {"finger_joint": metadata.get("finger_open_joint_angle")} + + # Store environment reference for later use + self._env = env + + # Grasp candidates will be generated lazily when first called + self.grasp_candidates = None + + # Initialize pose markers for visualization + if self.visualize_grasps: + frame_marker_cfg = FRAME_MARKER_CFG.copy() # type: ignore + frame_marker_cfg.markers["frame"].scale = ( + self.visualization_scale, + self.visualization_scale, + self.visualization_scale, + ) + self.pose_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/grasp_poses")) + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + object_cfg: SceneEntityCfg, + gripper_cfg: SceneEntityCfg, + num_candidates: int, + num_standoff_samples: int, + num_orientations: int, + lateral_sigma: float, + visualize_grasps: bool = False, + visualization_scale: float = 0.01, + ) -> None: + """Execute grasp sampling event - sample from pre-computed candidates.""" + # Generate grasp candidates if not already done + if self.grasp_candidates is None: + candidates_list = self._generate_grasp_candidates() + # Convert to tensor for efficient indexing + self.grasp_candidates = torch.stack( + [torch.tensor(candidate, dtype=torch.float32, device=env.device) for candidate in candidates_list] + ) + + # Visualize grasp poses if requested + if self.visualize_grasps: + self._visualize_grasp_poses(env, self.visualization_scale) + + # Get gripper from scene + gripper_asset = env.scene[self.gripper_cfg.name] + # First: Check for and fix any abnormal states before positioning + self._ensure_stable_gripper_state(env, gripper_asset, env_ids) + # Second: Open gripper to prepare for grasping + self._open_gripper(env, gripper_asset, env_ids) + # Randomly sample grasp candidates for the environments being reset + num_envs_reset = len(env_ids) + grasp_indices = torch.randint(0, len(self.grasp_candidates), (num_envs_reset,), device=env.device) + + # Apply grasp transforms to gripper (vectorized for multiple environments) + sampled_transforms = self.grasp_candidates[grasp_indices] + self._apply_grasp_transforms_vectorized(env, gripper_asset, sampled_transforms, env_ids) + + # Store grasp candidates for later evaluation + if not hasattr(env, "grasp_candidates"): + env.grasp_candidates = self.grasp_candidates + env.current_grasp_idx = 0 + env.grasp_results = [] + + def _generate_grasp_candidates(self): + """Generate grasp candidates using antipodal grasp sampling.""" + object_asset = self._env.scene[self.object_cfg.name] + mesh = self._extract_mesh_from_asset(object_asset) + grasp_transforms = self._sample_antipodal_grasps(mesh) + return grasp_transforms + + def _extract_mesh_from_asset(self, asset): + """Extract trimesh from IsaacLab asset.""" + # Get USD stage and prim path from the asset + stage = omni.usd.get_context().get_stage() + + # For multi-environment setups, we need to get the first environment's path + prim_path = asset.cfg.prim_path.replace(".*", "0", 1) + + # Get the USD prim + prim = stage.GetPrimAtPath(prim_path) + + # Find mesh geometry in the prim hierarchy + mesh_schema = self._find_mesh_in_prim(prim) + + # Convert USD mesh to trimesh + return self._usd_mesh_to_trimesh(mesh_schema) + + def _find_mesh_in_prim(self, prim): + """Find the first mesh under a prim.""" + if prim.IsA(UsdGeom.Mesh): + return UsdGeom.Mesh(prim) + + from pxr import Usd + + for child in Usd.PrimRange(prim): + if child.IsA(UsdGeom.Mesh): + return UsdGeom.Mesh(child) + return None + + def _usd_mesh_to_trimesh(self, usd_mesh): + """Convert USD mesh to trimesh for grasp sampling.""" + # Get vertices + points_attr = usd_mesh.GetPointsAttr() + vertices = torch.tensor(points_attr.Get(), dtype=torch.float32) + max_distance = torch.max(torch.norm(vertices, dim=1)) + # if the max distance is greater than 1.0, then the mesh is in mm + if max_distance > 1.0: + vertices = vertices / 1000.0 + + # Get faces + face_indices_attr = usd_mesh.GetFaceVertexIndicesAttr() + face_counts_attr = usd_mesh.GetFaceVertexCountsAttr() + + vertex_indices = torch.tensor(face_indices_attr.Get(), dtype=torch.long) + vertex_counts = torch.tensor(face_counts_attr.Get(), dtype=torch.long) + + # Convert to triangles + triangles = [] + offset = 0 + for count in vertex_counts: + indices = vertex_indices[offset : offset + count] + if count == 3: + triangles.append(indices.numpy()) + elif count == 4: + # Split quad into two triangles + triangles.extend([indices[[0, 1, 2]].numpy(), indices[[0, 2, 3]].numpy()]) + offset += count + + faces = torch.tensor(np.array(triangles), dtype=torch.long) + return trimesh.Trimesh(vertices=vertices.numpy(), faces=faces.numpy(), process=False) + + def _sample_antipodal_grasps(self, mesh): + """Sample antipodal grasp poses on a mesh using proper gripper parameterization.""" + # Extract parameters with defaults + num_surface_samples = max(1, int(self.num_candidates // (self.num_orientations * self.num_standoff_samples))) + + # Normalize input vectors using torch + gripper_approach_direction = torch.tensor(self.gripper_approach_direction, dtype=torch.float32) + gripper_approach_direction = gripper_approach_direction / torch.norm(gripper_approach_direction) + + grasp_align_axis = torch.tensor(self.grasp_align_axis, dtype=torch.float32) + grasp_align_axis = grasp_align_axis / torch.norm(grasp_align_axis) + + orientation_sample_axis = torch.tensor(self.orientation_sample_axis, dtype=torch.float32) + orientation_sample_axis = orientation_sample_axis / torch.norm(orientation_sample_axis) + + # Simple mesh-adaptive standoff: use bounding box diagonal for size-aware clearance + mesh_extents = mesh.extents + mesh_diagonal = np.linalg.norm(mesh_extents) + + # Handle standoff distance(s) with mesh-adaptive bonus + standoff_distances = torch.linspace( + self.finger_offset, + self.finger_offset + mesh_diagonal + self.finger_clearance / 2, + self.num_standoff_samples, + ) + + max_gripper_width = self.gripper_maximum_aperture + + # Sample more points initially to allow for top-bias filtering + initial_sample_size = num_surface_samples * 10 # Sample 10x more for filtering + surface_points, face_indices = mesh.sample(initial_sample_size, return_index=True) + surface_normals = mesh.face_normals[face_indices] + + # Bias toward top surfaces: prioritize points with higher Z coordinates and upward-facing normals + z_coords = surface_points[:, 2] + normal_z_components = surface_normals[:, 2] # Z component of surface normals + + # Calculate top-bias scores (higher Z + upward normal = higher score) + z_normalized = (z_coords - z_coords.min()) / (z_coords.max() - z_coords.min() + 1e-8) + normal_score = np.maximum(normal_z_components, 0) # Only positive Z normals + top_bias_scores = z_normalized + normal_score + + # Select top-biased subset + top_indices = np.argsort(top_bias_scores)[-num_surface_samples:] + surface_points = surface_points[top_indices] + surface_normals = surface_normals[top_indices] + + # Cast rays in opposite direction of the surface normal + ray_directions = -surface_normals + ray_intersections, ray_indices, _ = mesh.ray.intersects_location( + surface_points, ray_directions, multiple_hits=True + ) + + grasp_transforms = [] + + # Process each sampled point to find valid grasp candidates + for point_idx in range(len(surface_points)): + # Find intersection points for this ray + ray_hits = ray_intersections[ray_indices == point_idx] + + if len(ray_hits) == 0: + continue + + # Find the furthest intersection point for more stable grasps + if len(ray_hits) > 1: + distances = torch.norm(torch.tensor(ray_hits) - torch.tensor(surface_points[point_idx]), dim=1) + valid_indices = torch.where(distances <= max_gripper_width)[0] + if len(valid_indices) > 0: + furthest_idx = valid_indices[torch.argmax(distances[valid_indices])] + opposing_point = ray_hits[furthest_idx] + else: + continue + else: + opposing_point = ray_hits[0] + distance = torch.norm(torch.tensor(opposing_point) - torch.tensor(surface_points[point_idx])) + if distance > max_gripper_width: + continue + + # Calculate grasp axis and distance + grasp_axis = opposing_point - surface_points[point_idx] + axis_length = torch.norm(torch.tensor(grasp_axis)) + + if axis_length > trimesh.tol.zero and axis_length <= max_gripper_width: + grasp_axis = grasp_axis / axis_length.numpy() + + # Calculate grasp center with optional lateral perturbation + if self.lateral_sigma > 0: + midpoint_ratio = 0.5 + sigma_ratio = self.lateral_sigma / axis_length.numpy() + a = (0.0 - midpoint_ratio) / sigma_ratio + b = (1.0 - midpoint_ratio) / sigma_ratio + truncated_dist = stats.truncnorm(a, b, loc=midpoint_ratio, scale=sigma_ratio) + center_offset_ratio = truncated_dist.rvs() + grasp_center = surface_points[point_idx] + grasp_axis * axis_length.numpy() * center_offset_ratio + else: + grasp_center = surface_points[point_idx] + grasp_axis * axis_length.numpy() * 0.5 + + # Generate different orientations around each grasp axis + rotation_angles = torch.linspace(-torch.pi, torch.pi, self.num_orientations) + + for angle in rotation_angles: + # Align the gripper's grasp_align_axis with the computed grasp axis + align_matrix = trimesh.geometry.align_vectors(grasp_align_axis.numpy(), grasp_axis) + center_transform = tra.translation_matrix(grasp_center) + + # Create orientation transformation + orient_tf_rot = tra.rotation_matrix(angle=angle.item(), direction=orientation_sample_axis.numpy()) + + # Generate transforms for each standoff distance + for standoff_dist in standoff_distances: + standoff_translation = gripper_approach_direction.numpy() * -float(standoff_dist) + standoff_transform = tra.translation_matrix(standoff_translation) + + # Full transform: T_center * R_align * R_orient * T_standoff + align_mat = torch.tensor(align_matrix, dtype=torch.float32) + full_orientation_tf = torch.matmul(align_mat, torch.tensor(orient_tf_rot, dtype=torch.float32)) + full_orientation_tf = torch.matmul( + full_orientation_tf, torch.tensor(standoff_transform, dtype=torch.float32) + ) + grasp_world_tf = torch.matmul( + torch.tensor(center_transform, dtype=torch.float32), full_orientation_tf + ) + grasp_transforms.append(grasp_world_tf.numpy()) + + return grasp_transforms + + def _apply_grasp_transform_to_gripper(self, env, gripper_asset, grasp_transform, env_idx): + """Apply grasp transform to gripper asset.""" + # Get object's current pose in world coordinates + object_asset = env.scene[self.object_cfg.name] + object_pos = object_asset.data.root_pos_w[env_idx] + object_quat = object_asset.data.root_quat_w[env_idx] + + # Convert numpy transform matrix to torch tensors (object-local coordinates) + transform_tensor = torch.tensor(grasp_transform, dtype=torch.float32, device=env.device) + local_pos = transform_tensor[:3, 3] + rotation_matrix = transform_tensor[:3, :3] + local_quat = math_utils.quat_from_matrix(rotation_matrix.unsqueeze(0))[0] # (w, x, y, z) + + # Transform from object-local to world coordinates + world_pos, world_quat = math_utils.combine_frame_transforms( + object_pos.unsqueeze(0), object_quat.unsqueeze(0), local_pos.unsqueeze(0), local_quat.unsqueeze(0) + ) + + # Apply world transform to gripper asset for the specific environment + gripper_asset.data.root_pos_w[env_idx] = world_pos[0] + gripper_asset.data.root_quat_w[env_idx] = world_quat[0] + + # Write the new pose to simulation + indices = torch.tensor([env_idx], device=env.device) + root_pose = torch.cat([gripper_asset.data.root_pos_w[indices], gripper_asset.data.root_quat_w[indices]], dim=-1) + gripper_asset.write_root_pose_to_sim(root_pose, env_ids=indices) + + def _apply_grasp_transforms_vectorized(self, env, gripper_asset, grasp_transforms, env_ids): + """Apply grasp transforms to gripper assets for multiple environments (vectorized).""" + # Get object's current pose in world coordinates for all environments + object_asset = env.scene[self.object_cfg.name] + object_pos = object_asset.data.root_pos_w[env_ids] + object_quat = object_asset.data.root_quat_w[env_ids] + + # Extract positions and quaternions from transform matrices (already tensors) + local_positions = grasp_transforms[:, :3, 3] # Extract translation + rotation_matrices = grasp_transforms[:, :3, :3] # Extract rotation + local_quaternions = math_utils.quat_from_matrix(rotation_matrices) # (N, 4) in (w, x, y, z) + + # Transform from object-local to world coordinates (vectorized) + world_positions, world_quaternions = math_utils.combine_frame_transforms( + object_pos, object_quat, local_positions, local_quaternions + ) + + # Apply world transforms to gripper assets (vectorized) + gripper_asset.data.root_pos_w[env_ids] = world_positions + gripper_asset.data.root_quat_w[env_ids] = world_quaternions + + # Write the new poses to simulation (single vectorized call) + root_poses = torch.cat([world_positions, world_quaternions], dim=-1) + gripper_asset.write_root_pose_to_sim(root_poses, env_ids=env_ids) + + def _visualize_grasp_poses(self, env, scale: float = 0.03): + """Visualize all grasp poses using pose markers.""" + if self.grasp_candidates is None or not hasattr(self, "pose_marker"): + return + + # Get object asset for world transformation + object_asset = env.scene[self.object_cfg.name] + + # Get object's current pose in world coordinates + object_pos = object_asset.data.root_pos_w[0] # Use first environment + object_quat = object_asset.data.root_quat_w[0] # Use first environment + + # Convert grasp transforms to poses and transform to world coordinates + world_positions = [] + world_orientations = [] + + for transform in self.grasp_candidates: + # Extract position and rotation from transform matrix (object-local coordinates) + local_pos = transform[:3, 3].clone().detach().to(env.device) + rot_mat = transform[:3, :3].clone().detach().unsqueeze(0).to(env.device) + local_quat = math_utils.quat_from_matrix(rot_mat)[0] # (w, x, y, z) + + # Transform from object-local to world coordinates + world_pos, world_quat = math_utils.combine_frame_transforms( + object_pos.unsqueeze(0), object_quat.unsqueeze(0), local_pos.unsqueeze(0), local_quat.unsqueeze(0) + ) + + world_positions.append(world_pos[0]) + world_orientations.append(world_quat[0]) + + # Stack into final tensors + world_pos_tensor = torch.stack(world_positions) # Shape: (N, 3) + world_quat_tensor = torch.stack(world_orientations) # Shape: (N, 4) + + # Visualize using pose markers + self.pose_marker.visualize(world_pos_tensor, world_quat_tensor) + + def _open_gripper(self, env, gripper_asset, env_ids): + """Open gripper to prepare for grasping.""" + # Get current joint positions + current_joint_pos = gripper_asset.data.joint_pos[env_ids].clone() + + # Find joint indices using configurable joint names and positions + joint_configs = [] + for joint_name, target_position in self.gripper_joint_reset_config.items(): + if joint_name in gripper_asset.joint_names: + joint_idx = list(gripper_asset.joint_names).index(joint_name) + joint_configs.append((joint_idx, target_position)) + + if joint_configs: + # Set joints to their configured target positions + for env_idx_in_batch, env_id in enumerate(env_ids): + for joint_idx, target_position in joint_configs: + current_joint_pos[env_idx_in_batch, joint_idx] = target_position + + # Apply joint positions to simulation + gripper_asset.write_joint_state_to_sim( + position=current_joint_pos, + velocity=torch.zeros_like(current_joint_pos), + env_ids=env_ids, + ) + + def _ensure_stable_gripper_state(self, env, gripper_asset, env_ids): + """Comprehensively reset gripper to stable state before positioning.""" + # Always perform comprehensive reset to ensure clean state + # 1. Reset actuators to clear any accumulated forces/torques + gripper_asset.reset(env_ids) + + # 2. Reset to default root state (position and velocity) + default_root_state = gripper_asset.data.default_root_state[env_ids].clone() + default_root_state[:, 0:3] += env.scene.env_origins[env_ids] + gripper_asset.write_root_state_to_sim(default_root_state, env_ids=env_ids) + + # 3. Reset all joints to default positions with zero velocities + default_joint_pos = gripper_asset.data.default_joint_pos[env_ids].clone() + zero_joint_vel = torch.zeros_like(gripper_asset.data.default_joint_vel[env_ids]) + gripper_asset.write_joint_state_to_sim(default_joint_pos, zero_joint_vel, env_ids=env_ids) + + # 4. Set joint targets to default positions to prevent drift + gripper_asset.set_joint_position_target(default_joint_pos, env_ids=env_ids) + gripper_asset.set_joint_velocity_target(zero_joint_vel, env_ids=env_ids) + + +class global_physics_control_event(ManagerTermBase): + """Event class for global gravity and force/torque control based on synchronized timesteps.""" + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + self.gravity_on_interval = cfg.params.get("gravity_on_interval") + self.gravity_on_interval_s = ( + self.gravity_on_interval[0] / env.step_dt, + self.gravity_on_interval[1] / env.step_dt, + ) + self.force_torque_on_interval = cfg.params.get("force_torque_on_interval") + self.force_torque_on_interval_s = ( + self.force_torque_on_interval[0] / env.step_dt, + self.force_torque_on_interval[1] / env.step_dt, + ) + self.force_torque_asset_cfgs = cfg.params.get("force_torque_asset_cfgs", []) + self.force_torque_magnitude = cfg.params.get("force_torque_magnitude", 0.005) + self.physics_sim_view = sim_utils.SimulationContext.instance().physics_sim_view + + def reset(self, env_ids: torch.Tensor | None = None) -> None: + """Called when environments reset - disable gravity for positioning.""" + self.physics_sim_view.set_gravity(carb.Float3(0.0, 0.0, 0.0)) + self.gravity_enabled = False + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + gravity_on_interval: tuple[float, float], + force_torque_on_interval: tuple[float, float], + force_torque_asset_cfgs: list[SceneEntityCfg], + force_torque_magnitude: float, + ) -> None: + """Control global gravity based on timesteps since reset.""" + should_enable_gravity = ( + (env.episode_length_buf > self.gravity_on_interval_s[0]) + & (env.episode_length_buf < self.gravity_on_interval_s[1]) + ).any() + should_apply_force_torque = ( + (env.episode_length_buf > self.force_torque_on_interval_s[0]) + & (env.episode_length_buf < self.force_torque_on_interval_s[1]) + ).any() + + if should_enable_gravity and not self.gravity_enabled: + self.physics_sim_view.set_gravity(carb.Float3(0.0, 0.0, -9.81)) + self.gravity_enabled = True + elif not should_enable_gravity and self.gravity_enabled: + self.physics_sim_view.set_gravity(carb.Float3(0.0, 0.0, 0.0)) + self.gravity_enabled = False + else: + pass + + if should_apply_force_torque: + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device=env.device) + for asset_cfg in self.force_torque_asset_cfgs: + # extract the used quantities (to enable type-hinting) + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + # resolve number of bodies + num_bodies = len(asset_cfg.body_ids) if isinstance(asset_cfg.body_ids, list) else asset.num_bodies + + # Generate random forces in all directions + size = (len(env_ids), num_bodies, 3) + force_directions = torch.randn(size, device=asset.device) + force_directions = force_directions / torch.norm(force_directions, dim=-1, keepdim=True) + forces = force_directions * self.force_torque_magnitude + + # Generate independent random torques (pure rotational moments) + # These represent direct angular impulses rather than forces at lever arms + torque_directions = torch.randn(size, device=asset.device) + torque_directions = torque_directions / torch.norm(torque_directions, dim=-1, keepdim=True) + torques = torque_directions * self.force_torque_magnitude + + # 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) + + +class reset_end_effector_round_fixed_asset(ManagerTermBase): + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + fixed_asset_cfg: SceneEntityCfg = cfg.params.get("fixed_asset_cfg") # type: ignore + fixed_asset_offset: Offset = cfg.params.get("fixed_asset_offset") # type: ignore + pose_range_b: dict[str, tuple[float, float]] = cfg.params.get("pose_range_b") # type: ignore + robot_ik_cfg: SceneEntityCfg = cfg.params.get("robot_ik_cfg", SceneEntityCfg("robot")) + + range_list = [pose_range_b.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + self.ranges = torch.tensor(range_list, device=env.device) + self.fixed_asset: Articulation | RigidObject = env.scene[fixed_asset_cfg.name] + self.fixed_asset_offset: Offset = fixed_asset_offset + self.robot: Articulation = env.scene[robot_ik_cfg.name] + self.joint_ids: list[int] | slice = robot_ik_cfg.joint_ids + self.n_joints: int = self.robot.num_joints if isinstance(self.joint_ids, slice) else len(self.joint_ids) + robot_ik_solver_cfg = DifferentialInverseKinematicsActionCfg( + asset_name=robot_ik_cfg.name, + joint_names=robot_ik_cfg.joint_names, # type: ignore + body_name=robot_ik_cfg.body_names, # type: ignore + controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls"), + scale=1.0, + ) + self.solver: DifferentialInverseKinematicsAction = robot_ik_solver_cfg.class_type(robot_ik_solver_cfg, env) # type: ignore + self.reset_velocity = torch.zeros((env.num_envs, self.robot.data.joint_vel.shape[1]), device=env.device) + self.reset_position = torch.zeros((env.num_envs, self.robot.data.joint_pos.shape[1]), device=env.device) + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + fixed_asset_cfg: SceneEntityCfg, + fixed_asset_offset: Offset, + pose_range_b: dict[str, tuple[float, float]], + robot_ik_cfg: SceneEntityCfg, + ) -> None: + if fixed_asset_offset is None: + fixed_tip_pos_w, fixed_tip_quat_w = ( + env.scene[fixed_asset_cfg.name].data.root_pos_w, + env.scene[fixed_asset_cfg.name].data.root_quat_w, + ) + else: + fixed_tip_pos_w, fixed_tip_quat_w = self.fixed_asset_offset.apply(self.fixed_asset) + + samples = math_utils.sample_uniform(self.ranges[:, 0], self.ranges[:, 1], (env.num_envs, 6), device=env.device) + pos_b, quat_b = self.solver._compute_frame_pose() + # for those non_reset_id, we will let ik solve for its current position + pos_w = fixed_tip_pos_w + samples[:, 0:3] + quat_w = math_utils.quat_from_euler_xyz(samples[:, 3], samples[:, 4], samples[:, 5]) + pos_b, quat_b = math_utils.subtract_frame_transforms( + self.robot.data.root_link_pos_w, self.robot.data.root_link_quat_w, pos_w, quat_w + ) + self.solver.process_actions(torch.cat([pos_b, quat_b], dim=1)) + + # Error Rate 75% ^ 10 = 0.05 (final error) + for i in range(10): + self.solver.apply_actions() + delta_joint_pos = 0.25 * (self.robot.data.joint_pos_target[env_ids] - self.robot.data.joint_pos[env_ids]) + self.robot.write_joint_state_to_sim( + position=(delta_joint_pos + self.robot.data.joint_pos[env_ids])[:, self.joint_ids], + velocity=torch.zeros((len(env_ids), self.n_joints), device=env.device), + joint_ids=self.joint_ids, + env_ids=env_ids, # type: ignore + ) + + +class reset_end_effector_from_grasp_dataset(ManagerTermBase): + """Reset end effector pose using saved grasp dataset from grasp sampling.""" + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + self.base_path: str = cfg.params.get("base_path") + self.fixed_asset_cfg: SceneEntityCfg = cfg.params.get("fixed_asset_cfg") # type: ignore + robot_ik_cfg: SceneEntityCfg = cfg.params.get("robot_ik_cfg", SceneEntityCfg("robot")) + gripper_cfg: SceneEntityCfg = cfg.params.get( + "gripper_cfg", SceneEntityCfg("robot", joint_names=["finger_joint"]) + ) + # Set up robot and IK solver for arm joints + self.fixed_asset: Articulation | RigidObject = env.scene[self.fixed_asset_cfg.name] + self.robot: Articulation = env.scene[robot_ik_cfg.name] + self.joint_ids: list[int] | slice = robot_ik_cfg.joint_ids + self.n_joints: int = self.robot.num_joints if isinstance(self.joint_ids, slice) else len(self.joint_ids) + + # Pose range for sampling variations + pose_range_b: dict[str, tuple[float, float]] = cfg.params.get("pose_range_b", dict()) + range_list = [pose_range_b.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + self.ranges = torch.tensor(range_list, device=env.device) + + robot_ik_solver_cfg = DifferentialInverseKinematicsActionCfg( + asset_name=robot_ik_cfg.name, + joint_names=robot_ik_cfg.joint_names, # type: ignore + body_name=robot_ik_cfg.body_names, # type: ignore + controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls"), + scale=1.0, + ) + self.solver: DifferentialInverseKinematicsAction = robot_ik_solver_cfg.class_type(robot_ik_solver_cfg, env) # type: ignore + + # Set up gripper joint control separately + self.gripper: Articulation = env.scene[ + gripper_cfg.name + ] # Should be same as robot but different joint selection + self.gripper_joint_ids: list[int] | slice = gripper_cfg.joint_ids + self.gripper_joint_names: list[str] = gripper_cfg.joint_names if gripper_cfg.joint_names else [] + + # Compute grasp dataset path using object hash + self.grasp_dataset_path = self._compute_grasp_dataset_path() + + # Load and pre-compute grasp data for fast sampling + self._load_and_precompute_grasps(env) + + def _compute_grasp_dataset_path(self) -> str: + """Compute grasp dataset path using hash of the fixed asset (insertive object).""" + usd_path = self.fixed_asset.cfg.spawn.usd_path + object_hash = utils.compute_assembly_hash(usd_path) + return f"{self.base_path}/{object_hash}.pt" + + def _load_and_precompute_grasps(self, env): + """Load Torch (.pt) grasp data and convert to optimized tensors.""" + # Handle URL or local path + local_path = retrieve_file_path(self.grasp_dataset_path) + data = torch.load(local_path, map_location="cpu") + + # TorchDatasetFileHandler stores nested dicts; grasp data likely under 'grasp_relative_pose' + grasp_group = data.get("grasp_relative_pose", data) + + rel_pos_list = grasp_group.get("relative_position", []) + rel_quat_list = grasp_group.get("relative_orientation", []) + gripper_joint_positions_dict = grasp_group.get("gripper_joint_positions", {}) + + num_grasps = len(rel_pos_list) + if num_grasps == 0: + raise ValueError(f"No grasp data found in {self.grasp_dataset_path}") + + # Convert positions and orientations to tensors on env device + self.rel_positions = torch.stack( + [ + (pos if isinstance(pos, torch.Tensor) else torch.as_tensor(pos, dtype=torch.float32)) + for pos in rel_pos_list + ], + dim=0, + ).to(env.device, dtype=torch.float32) + + self.rel_quaternions = torch.stack( + [ + (quat if isinstance(quat, torch.Tensor) else torch.as_tensor(quat, dtype=torch.float32)) + for quat in rel_quat_list + ], + dim=0, + ).to(env.device, dtype=torch.float32) + + # Get gripper joint mapping + if isinstance(self.gripper_joint_ids, slice): + gripper_joint_list = list(range(self.robot.num_joints))[self.gripper_joint_ids] + else: + gripper_joint_list = self.gripper_joint_ids + + num_gripper_joints = len(gripper_joint_list) + self.gripper_joint_positions = torch.zeros( + (num_grasps, num_gripper_joints), device=env.device, dtype=torch.float32 + ) + + # Build joint matrix ordered by robot joint indices per provided gripper_joint_ids + for gripper_idx, robot_joint_idx in enumerate(gripper_joint_list): + joint_name = self.robot.joint_names[robot_joint_idx] + joint_series = gripper_joint_positions_dict.get(joint_name, [0.0] * num_grasps) + joint_tensor = torch.stack( + [(j if isinstance(j, torch.Tensor) else torch.as_tensor(j, dtype=torch.float32)) for j in joint_series], + dim=0, + ).to(env.device, dtype=torch.float32) + self.gripper_joint_positions[:, gripper_idx] = joint_tensor + + print(f"Loaded and pre-computed {num_grasps} grasp tensors from Torch file: {self.grasp_dataset_path}") + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + base_path: str, + fixed_asset_cfg: SceneEntityCfg, + robot_ik_cfg: SceneEntityCfg, + gripper_cfg: SceneEntityCfg, + pose_range_b: dict[str, tuple[float, float]] = dict(), + ) -> None: + """Apply grasp poses to reset end effector.""" + # RigidObject asset + object_pos_w = self.fixed_asset.data.root_pos_w[env_ids] + object_quat_w = self.fixed_asset.data.root_quat_w[env_ids] + + # Randomly sample grasp indices for each environment + num_envs = len(env_ids) + grasp_indices = torch.randint(0, len(self.rel_positions), (num_envs,), device=env.device) + + # Use pre-computed tensors for sampled grasps + sampled_rel_positions = self.rel_positions[grasp_indices] + sampled_rel_quaternions = self.rel_quaternions[grasp_indices] + + # Vectorized transform to world coordinates: T_gripper_world = T_object_world * T_relative + gripper_pos_w, gripper_quat_w = math_utils.combine_frame_transforms( + object_pos_w, object_quat_w, sampled_rel_positions, sampled_rel_quaternions + ) + + # Vectorized transform to robot base coordinates + pos_b, quat_b = self.solver._compute_frame_pose() + pos_b[env_ids], quat_b[env_ids] = math_utils.subtract_frame_transforms( + self.robot.data.root_link_pos_w[env_ids], + self.robot.data.root_link_quat_w[env_ids], + gripper_pos_w, + gripper_quat_w, + ) + + # Add pose variation sampling if ranges are specified (in body frame) + if torch.any(self.ranges != 0.0): + samples = math_utils.sample_uniform(self.ranges[:, 0], self.ranges[:, 1], (num_envs, 6), device=env.device) + pos_b[env_ids], quat_b[env_ids] = math_utils.combine_frame_transforms( + pos_b[env_ids], + quat_b[env_ids], + samples[:, 0:3], + math_utils.quat_from_euler_xyz(samples[:, 3], samples[:, 4], samples[:, 5]), + ) + + self.solver.process_actions(torch.cat([pos_b, quat_b], dim=1)) + + # Solve IK iteratively for better convergence + for i in range(25): + self.solver.apply_actions() + delta_joint_pos = 0.25 * (self.robot.data.joint_pos_target[env_ids] - self.robot.data.joint_pos[env_ids]) + self.robot.write_joint_state_to_sim( + position=(delta_joint_pos + self.robot.data.joint_pos[env_ids])[:, self.joint_ids], + velocity=torch.zeros((len(env_ids), self.n_joints), device=env.device), + joint_ids=self.joint_ids, + env_ids=env_ids, # type: ignore + ) + + # Sample gripper joint positions using the same indices + sampled_gripper_positions = self.gripper_joint_positions[grasp_indices] + + # Single vectorized write for all environments + self.robot.write_joint_state_to_sim( + position=sampled_gripper_positions, + velocity=torch.zeros_like(sampled_gripper_positions), + joint_ids=self.gripper_joint_ids, + env_ids=env_ids, + ) + + +class reset_insertive_object_from_partial_assembly_dataset(ManagerTermBase): + """EventTerm class for resetting the insertive object from a partial assembly dataset.""" + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + # Extract parameters from config + self.base_path: str = cfg.params.get("base_path") + self.receptive_object_cfg: SceneEntityCfg = cfg.params.get("receptive_object_cfg") + self.receptive_object: RigidObject = env.scene[self.receptive_object_cfg.name] + self.insertive_object_cfg: SceneEntityCfg = cfg.params.get("insertive_object_cfg") + self.insertive_object: RigidObject = env.scene[self.insertive_object_cfg.name] + + # Pose range for sampling variations + pose_range_b: dict[str, tuple[float, float]] = cfg.params.get("pose_range_b", dict()) + range_list = [pose_range_b.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + self.ranges = torch.tensor(range_list, device=env.device) + + # Compute partial assembly dataset path using object hash + self.partial_assembly_dataset_path = self._compute_partial_assembly_dataset_path() + + # Load and pre-compute partial assembly data for fast sampling + self._load_and_precompute_partial_assemblies(env) + + def _compute_partial_assembly_dataset_path(self) -> str: + """Compute partial assembly dataset path using hash of insertive and receptive objects.""" + insertive_usd_path = self.insertive_object.cfg.spawn.usd_path + receptive_usd_path = self.receptive_object.cfg.spawn.usd_path + object_hash = utils.compute_assembly_hash(insertive_usd_path, receptive_usd_path) + return f"{self.base_path}/{object_hash}.pt" + + def _load_and_precompute_partial_assemblies(self, env): + """Load Torch (.pt) partial assembly data and convert to optimized tensors.""" + local_path = retrieve_file_path(self.partial_assembly_dataset_path) + data = torch.load(local_path, map_location="cpu") + + rel_pos = data.get("relative_position") + rel_quat = data.get("relative_orientation") + + if rel_pos is None or rel_quat is None or len(rel_pos) == 0: + raise ValueError(f"No partial assembly data found in {self.partial_assembly_dataset_path}") + + # Tensors were saved via torch.save; ensure proper device/dtype + if not isinstance(rel_pos, torch.Tensor): + rel_pos = torch.as_tensor(rel_pos, dtype=torch.float32) + if not isinstance(rel_quat, torch.Tensor): + rel_quat = torch.as_tensor(rel_quat, dtype=torch.float32) + + self.rel_positions = rel_pos.to(env.device, dtype=torch.float32) + self.rel_quaternions = rel_quat.to(env.device, dtype=torch.float32) + + print( + f"Loaded {len(self.rel_positions)} partial assembly tensors from Torch file:" + f" {self.partial_assembly_dataset_path}" + ) + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + base_path: str, + insertive_object_cfg: SceneEntityCfg, + receptive_object_cfg: SceneEntityCfg, + pose_range_b: dict[str, tuple[float, float]] = dict(), + ) -> None: + """Reset the insertive object from a partial assembly dataset.""" + # Get receptive object pose (world coordinates) + receptive_pos_w = self.receptive_object.data.root_pos_w[env_ids] + receptive_quat_w = self.receptive_object.data.root_quat_w[env_ids] + + # Randomly sample partial assembly indices for each environment + num_envs = len(env_ids) + assembly_indices = torch.randint(0, len(self.rel_positions), (num_envs,), device=env.device) + + # Use pre-computed tensors for sampled partial assemblies + sampled_rel_positions = self.rel_positions[assembly_indices] + sampled_rel_quaternions = self.rel_quaternions[assembly_indices] + + # Vectorized transform to world coordinates: T_insertive_world = T_receptive_world * T_relative + insertive_pos_w, insertive_quat_w = math_utils.combine_frame_transforms( + receptive_pos_w, receptive_quat_w, sampled_rel_positions, sampled_rel_quaternions + ) + + # Add pose variation sampling if ranges are specified + if torch.any(self.ranges != 0.0): + samples = math_utils.sample_uniform(self.ranges[:, 0], self.ranges[:, 1], (num_envs, 6), device=env.device) + insertive_pos_w, insertive_quat_w = math_utils.combine_frame_transforms( + insertive_pos_w, + insertive_quat_w, + samples[:, 0:3], + math_utils.quat_from_euler_xyz(samples[:, 3], samples[:, 4], samples[:, 5]), + ) + + # Set insertive object pose + self.insertive_object.write_root_state_to_sim( + root_state=torch.cat( + [ + insertive_pos_w, + insertive_quat_w, + torch.zeros((num_envs, 6), device=env.device), # Zero linear and angular velocities + ], + dim=-1, + ), + env_ids=env_ids, + ) + + +class pose_logging_event(ManagerTermBase): + """EventTerm class for logging pose data from all environments.""" + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + self.receptive_object_cfg = cfg.params.get("receptive_object_cfg") + self.receptive_object = env.scene[self.receptive_object_cfg.name] + self.insertive_object_cfg = cfg.params.get("insertive_object_cfg") + self.insertive_object = env.scene[self.insertive_object_cfg.name] + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + receptive_object_cfg: SceneEntityCfg, + insertive_object_cfg: SceneEntityCfg, + ) -> None: + """Collect pose data from all environments.""" + + # Get object poses for all environments + receptive_pos = self.receptive_object.data.root_pos_w[env_ids] + receptive_quat = self.receptive_object.data.root_quat_w[env_ids] + insertive_pos = self.insertive_object.data.root_pos_w[env_ids] + insertive_quat = self.insertive_object.data.root_quat_w[env_ids] + + # Calculate relative transform + relative_pos, relative_quat = math_utils.subtract_frame_transforms( + receptive_pos, receptive_quat, insertive_pos, insertive_quat + ) + + # Store pose data for external access + if "log" not in env.extras: + env.extras["log"] = {} + env.extras["log"]["current_pose_data"] = { + "relative_position": relative_pos, + "relative_orientation": relative_quat, + "relative_pose": torch.cat([relative_pos, relative_quat], dim=-1), + "receptive_object_pose": torch.cat([receptive_pos, receptive_quat], dim=-1), + "insertive_object_pose": torch.cat([insertive_pos, insertive_quat], dim=-1), + } + + +class assembly_sampling_event(ManagerTermBase): + """EventTerm class for spawning insertive object at assembled offset position.""" + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + self.receptive_object_cfg = cfg.params.get("receptive_object_cfg") + self.receptive_object = env.scene[self.receptive_object_cfg.name] + self.insertive_object_cfg = cfg.params.get("insertive_object_cfg") + self.insertive_object = env.scene[self.insertive_object_cfg.name] + + insertive_metadata = utils.read_metadata_from_usd_directory(self.insertive_object.cfg.spawn.usd_path) + receptive_metadata = utils.read_metadata_from_usd_directory(self.receptive_object.cfg.spawn.usd_path) + + self.insertive_assembled_offset = Offset( + pos=insertive_metadata.get("assembled_offset").get("pos"), + quat=insertive_metadata.get("assembled_offset").get("quat"), + ) + self.receptive_assembled_offset = Offset( + pos=receptive_metadata.get("assembled_offset").get("pos"), + quat=receptive_metadata.get("assembled_offset").get("quat"), + ) + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + receptive_object_cfg: SceneEntityCfg, + insertive_object_cfg: SceneEntityCfg, + ) -> None: + """Spawn insertive object at assembled offset position.""" + + # Get receptive object poses + receptive_pos = self.receptive_object.data.root_pos_w[env_ids] + receptive_quat = self.receptive_object.data.root_quat_w[env_ids] + + # Apply receptive assembled offset to get target position + target_pos, target_quat = self.receptive_assembled_offset.combine(receptive_pos, receptive_quat) + + # Apply inverse insertive offset to get insertive object root position + insertive_pos, insertive_quat = self.insertive_assembled_offset.subtract(target_pos, target_quat) + + # Set insertive object pose + self.insertive_object.write_root_state_to_sim( + root_state=torch.cat( + [insertive_pos, insertive_quat, torch.zeros((len(env_ids), 6), device=env.device)], # Zero velocities + dim=-1, + ), + env_ids=env_ids, + ) + + +class MultiResetManager(ManagerTermBase): + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + base_paths: list[str] = cfg.params.get("base_paths", []) + probabilities: list[float] = cfg.params.get("probs", []) + + if not base_paths: + raise ValueError("No base paths provided") + if len(base_paths) != len(probabilities): + raise ValueError("Number of base paths must match number of probabilities") + + # Compute dataset paths using object hash + insertive_usd_path = env.scene["insertive_object"].cfg.spawn.usd_path + receptive_usd_path = env.scene["receptive_object"].cfg.spawn.usd_path + reset_state_hash = utils.compute_assembly_hash(insertive_usd_path, receptive_usd_path) + + # Generate dataset paths using provided base paths + dataset_files = [] + for base_path in base_paths: + dataset_files.append(f"{base_path}/{reset_state_hash}.pt") + + # Load all datasets + self.datasets = [] + num_states = [] + rank = int(os.getenv("RANK", "0")) + download_dir = os.path.join(tempfile.gettempdir(), f"rank_{rank}") + for dataset_file in dataset_files: + # Handle both local files and URLs + local_file_path = retrieve_file_path(dataset_file, download_dir=download_dir) + + # Check if local file exists (after potential download) + if not os.path.exists(local_file_path): + raise FileNotFoundError(f"Dataset file {dataset_file} could not be accessed or downloaded.") + + dataset = torch.load(local_file_path) + num_states.append(len(dataset["initial_state"]["articulation"]["robot"]["joint_position"])) + init_indices = torch.arange(num_states[-1], device=env.device) + self.datasets.append(sample_state_data_set(dataset, init_indices, env.device)) + + # Normalize probabilities and store dataset lengths + self.probs = torch.tensor(probabilities, device=env.device) / sum(probabilities) + self.num_states = torch.tensor(num_states, device=env.device) + self.num_tasks = len(self.datasets) + + # Initialize success monitor + if cfg.params.get("success") is not None: + success_monitor_cfg = SuccessMonitorCfg( + monitored_history_len=100, num_monitored_data=self.num_tasks, device=env.device + ) + self.success_monitor = success_monitor_cfg.class_type(success_monitor_cfg) + + self.task_id = torch.randint(0, self.num_tasks, (self.num_envs,), device=self.device) + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + base_paths: list[str], + probs: list[float], + success: str | None = None, + ) -> None: + if env_ids is None: + env_ids = torch.arange(self.num_envs, device=self._env.device) + + # Log current data + if success is not None: + success_mask = torch.where(eval(success)[env_ids], 1.0, 0.0) + self.success_monitor.success_update(self.task_id[env_ids], success_mask) + + # Log metrics for each task + success_rates = self.success_monitor.get_success_rate() + if "log" not in self._env.extras: + self._env.extras["log"] = {} + for task_idx in range(self.num_tasks): + self._env.extras["log"].update({ + f"Metrics/task_{task_idx}_success_rate": success_rates[task_idx].item(), + f"Metrics/task_{task_idx}_prob": self.probs[task_idx].item(), + f"Metrics/task_{task_idx}_normalized_prob": self.probs[task_idx].item(), + }) + + # Sample which dataset to use for each environment + dataset_indices = torch.multinomial(self.probs, len(env_ids), replacement=True) + self.task_id[env_ids] = dataset_indices + + # Process each dataset's environments + for dataset_idx in range(self.num_tasks): + mask = dataset_indices == dataset_idx + if not mask.any(): + continue + + current_env_ids = env_ids[mask] + state_indices = torch.randint( + 0, self.num_states[dataset_idx], (len(current_env_ids),), device=self._env.device + ) + states_to_reset_from = sample_from_nested_dict(self.datasets[dataset_idx], state_indices) + self._env.scene.reset_to(states_to_reset_from["initial_state"], env_ids=current_env_ids, is_relative=True) + + # Reset velocities + robot: Articulation = self._env.scene["robot"] + robot.set_joint_velocity_target(torch.zeros_like(robot.data.joint_vel[env_ids]), env_ids=env_ids) + + +def sample_state_data_set(episode_data: dict, idx: torch.Tensor, device: torch.device) -> dict: + """Sample state from episode data and move tensors to device in one pass.""" + result = {} + for key, value in episode_data.items(): + if isinstance(value, dict): + result[key] = sample_state_data_set(value, idx, device) + elif isinstance(value, list): + result[key] = torch.stack([value[i] for i in idx.tolist()], dim=0).to(device) + else: + raise TypeError(f"Unsupported type in episode data: {type(value)}") + return result + + +def sample_from_nested_dict(nested_dict: dict, idx) -> dict: + """Extract elements from a nested dictionary using given indices.""" + sampled_dict = {} + for key, value in nested_dict.items(): + if isinstance(value, dict): + sampled_dict[key] = sample_from_nested_dict(value, idx) + elif isinstance(value, torch.Tensor): + sampled_dict[key] = value[idx].clone() + else: + raise TypeError(f"Unsupported type in nested dictionary: {type(value)}") + return sampled_dict + + +class reset_root_states_uniform(ManagerTermBase): + """Reset multiple assets' root states to random positions and velocities uniformly within given ranges. + + This function randomizes the root position and velocity of multiple assets using the same random offsets. + This keeps the relative positioning between assets intact while randomizing their global position. + + * It samples the root position from the given ranges and adds them to each asset's default root position + * It samples the root orientation from the given ranges and sets them into the physics simulation + * It samples the root velocity from the given ranges and sets them into the physics simulation + + The function takes a dictionary of pose and velocity ranges for each axis and rotation. The keys of the + dictionary are ``x``, ``y``, ``z``, ``roll``, ``pitch``, and ``yaw``. The values are tuples of the form + ``(min, max)``. If the dictionary does not contain a key, the position or velocity is set to zero for that axis. + + Args: + env: The environment instance + env_ids: The environment IDs to reset + pose_range: Dictionary of position and orientation ranges + velocity_range: Dictionary of linear and angular velocity ranges + asset_cfgs: List of asset configurations to reset (all receive same random offset) + """ + + def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + pose_range_dict = cfg.params.get("pose_range") + velocity_range_dict = cfg.params.get("velocity_range") + + self.pose_range = torch.tensor( + [pose_range_dict.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]], device=env.device + ) + self.velocity_range = torch.tensor( + [velocity_range_dict.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]], + device=env.device, + ) + self.asset_cfgs = list(cfg.params.get("asset_cfgs", dict()).values()) + self.offset_asset_cfg = cfg.params.get("offset_asset_cfg") + self.use_bottom_offset = cfg.params.get("use_bottom_offset", False) + + if self.use_bottom_offset: + self.bottom_offset_positions = dict() + for asset_cfg in self.asset_cfgs: + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + usd_path = asset.cfg.spawn.usd_path + metadata = utils.read_metadata_from_usd_directory(usd_path) + bottom_offset = metadata.get("bottom_offset") + self.bottom_offset_positions[asset_cfg.name] = ( + torch.tensor(bottom_offset.get("pos"), device=env.device).unsqueeze(0).repeat(env.num_envs, 1) + ) + assert tuple(bottom_offset.get("quat")) == ( + 1.0, + 0.0, + 0.0, + 0.0, + ), "Bottom offset rotation must be (1.0, 0.0, 0.0, 0.0)" + + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor, + pose_range: dict[str, tuple[float, float]], + velocity_range: dict[str, tuple[float, float]], + asset_cfgs: dict[str, SceneEntityCfg] = dict(), + offset_asset_cfg: SceneEntityCfg = None, + use_bottom_offset: bool = False, + ) -> None: + # poses + rand_pose_samples = math_utils.sample_uniform( + self.pose_range[:, 0], self.pose_range[:, 1], (len(env_ids), 6), device=env.device + ) + + # Create orientation delta quaternion from the random Euler angles + orientations_delta = math_utils.quat_from_euler_xyz( + rand_pose_samples[:, 3], rand_pose_samples[:, 4], rand_pose_samples[:, 5] + ) + + # velocities + rand_vel_samples = math_utils.sample_uniform( + self.velocity_range[:, 0], self.velocity_range[:, 1], (len(env_ids), 6), device=env.device + ) + + # Apply the same random offsets to each asset + for asset_cfg in self.asset_cfgs: + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + + # Get default root state for this asset + root_states = asset.data.default_root_state[env_ids].clone() + + # Apply position offset + positions = root_states[:, 0:3] + env.scene.env_origins[env_ids] + rand_pose_samples[:, 0:3] + + if self.offset_asset_cfg: + offset_asset: RigidObject | Articulation = env.scene[self.offset_asset_cfg.name] + offset_positions = offset_asset.data.default_root_state[env_ids].clone() + positions += offset_positions[:, 0:3] + + if self.use_bottom_offset: + bottom_offset_position = self.bottom_offset_positions[asset_cfg.name] + positions -= bottom_offset_position[env_ids, 0:3] + + # Apply orientation offset + orientations = math_utils.quat_mul(root_states[:, 3:7], orientations_delta) + + # Apply velocity offset + velocities = root_states[:, 7:13] + rand_vel_samples + + # Set the new pose and velocity into the physics simulation + 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) + + +def randomize_operational_space_controller_gains( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + action_name: str, + stiffness_distribution_params: tuple[float, float], + damping_distribution_params: tuple[float, float], + operation: str = "scale", + distribution: str = "log_uniform", +) -> None: + """Randomize operational space controller motion stiffness and damping gains. + + This function randomizes the motion_stiffness_task and motion_damping_ratio_task parameters + of an operational space controller. The first three terms (xyz) and last three terms (ypr) + are randomized together to maintain consistency within translational and rotational components. + + Args: + env: The environment instance. + env_ids: The environment indices to randomize. If None, all environments are randomized. + action_name: The name of the action term to randomize. + stiffness_distribution_params: The distribution parameters for stiffness (min, max). + damping_distribution_params: The distribution parameters for damping ratio (min, max). + operation: The operation to perform on the gains. Currently supports "scale" and "add". + distribution: The distribution to sample from. Currently supports "log_uniform". + + Raises: + ValueError: If the action is not found or is not an operational space controller action. + ValueError: If an unsupported distribution is specified. + """ + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device=env.device) + + # Get the action term + action_term = env.action_manager._terms.get(action_name) + if action_term is None: + raise ValueError(f"Action term '{action_name}' not found in action manager.") + + # Check if it's an operational space controller action + if not hasattr(action_term, "_osc") or not hasattr(action_term._osc, "cfg"): + raise ValueError(f"Action term '{action_name}' does not appear to be an operational space controller.") + + controller = action_term._osc + + # Check distribution type + if operation != "scale": + raise ValueError(f"Operation '{operation}' not supported. Only 'scale' is supported.") + if distribution not in ["uniform", "log_uniform"]: + raise ValueError( + f"Distribution '{distribution}' not supported. Only 'uniform' and 'log_uniform' are supported." + ) + + # Sample random multipliers for stiffness (xyz and ypr separately) + if distribution == "uniform": + stiff_xyz_multiplier = ( + torch.rand(len(env_ids), device=env.device) + * (stiffness_distribution_params[1] - stiffness_distribution_params[0]) + + stiffness_distribution_params[0] + ) + + stiff_rpy_multiplier = ( + torch.rand(len(env_ids), device=env.device) + * (stiffness_distribution_params[1] - stiffness_distribution_params[0]) + + stiffness_distribution_params[0] + ) + else: # log_uniform + log_min_stiff = torch.log(torch.tensor(stiffness_distribution_params[0], device=env.device)) + log_max_stiff = torch.log(torch.tensor(stiffness_distribution_params[1], device=env.device)) + + stiff_xyz_multiplier = torch.exp( + torch.rand(len(env_ids), device=env.device) * (log_max_stiff - log_min_stiff) + log_min_stiff + ) + + stiff_rpy_multiplier = torch.exp( + torch.rand(len(env_ids), device=env.device) * (log_max_stiff - log_min_stiff) + log_min_stiff + ) + + # Sample random multipliers for damping (xyz and ypr separately) + if distribution == "uniform": + damp_xyz_multiplier = ( + torch.rand(len(env_ids), device=env.device) + * (damping_distribution_params[1] - damping_distribution_params[0]) + + damping_distribution_params[0] + ) + + damp_rpy_multiplier = ( + torch.rand(len(env_ids), device=env.device) + * (damping_distribution_params[1] - damping_distribution_params[0]) + + damping_distribution_params[0] + ) + else: # log_uniform + log_min_damp = torch.log(torch.tensor(damping_distribution_params[0], device=env.device)) + log_max_damp = torch.log(torch.tensor(damping_distribution_params[1], device=env.device)) + + damp_xyz_multiplier = torch.exp( + torch.rand(len(env_ids), device=env.device) * (log_max_damp - log_min_damp) + log_min_damp + ) + + damp_rpy_multiplier = torch.exp( + torch.rand(len(env_ids), device=env.device) * (log_max_damp - log_min_damp) + log_min_damp + ) + + # Apply randomization to motion stiffness gains + # Original gains from config + original_stiffness = torch.tensor(controller.cfg.motion_stiffness_task, device=env.device) + + # Create new stiffness values for each environment + new_stiffness = torch.zeros((len(env_ids), 6), device=env.device) + new_stiffness[:, 0:3] = original_stiffness[0:3] * stiff_xyz_multiplier.unsqueeze(-1) # xyz + new_stiffness[:, 3:6] = original_stiffness[3:6] * stiff_rpy_multiplier.unsqueeze(-1) # rpy + + # Update the controller's motion stiffness gains + controller._motion_p_gains_task[env_ids] = torch.diag_embed(new_stiffness) + # Apply selection matrix to zero out non-controlled axes + controller._motion_p_gains_task[env_ids] = ( + controller._selection_matrix_motion_task[env_ids] @ controller._motion_p_gains_task[env_ids] + ) + + # Apply randomization to motion damping gains + # Original damping ratios from config + original_damping = torch.tensor(controller.cfg.motion_damping_ratio_task, device=env.device) + + # Create new damping values for each environment + new_damping_ratios = torch.zeros((len(env_ids), 6), device=env.device) + new_damping_ratios[:, 0:3] = original_damping[0:3] * damp_xyz_multiplier.unsqueeze(-1) # xyz + new_damping_ratios[:, 3:6] = original_damping[3:6] * damp_rpy_multiplier.unsqueeze(-1) # rpy + + # Update the controller's motion damping gains + # Damping = 2 * sqrt(stiffness) * damping_ratio + controller._motion_d_gains_task[env_ids] = torch.diag_embed( + 2 * torch.diagonal(controller._motion_p_gains_task[env_ids], dim1=-2, dim2=-1).sqrt() * new_damping_ratios + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/observations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/observations.py new file mode 100644 index 0000000..d2eb8ec --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/observations.py @@ -0,0 +1,201 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation, RigidObject +from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv +from isaaclab.managers import ManagerTermBase, ObservationTermCfg, SceneEntityCfg + +from uwlab_tasks.manager_based.manipulation.reset_states.assembly_keypoints import Offset +from uwlab_tasks.manager_based.manipulation.reset_states.mdp import utils + + +def target_asset_pose_in_root_asset_frame( + env: ManagerBasedEnv, + target_asset_cfg: SceneEntityCfg, + root_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + target_asset_offset=None, + root_asset_offset=None, + rotation_repr: str = "quat", +): + target_asset: RigidObject | Articulation = env.scene[target_asset_cfg.name] + root_asset: RigidObject | Articulation = env.scene[root_asset_cfg.name] + + target_body_idx = 0 if isinstance(target_asset_cfg.body_ids, slice) else target_asset_cfg.body_ids + root_body_idx = 0 if isinstance(root_asset_cfg.body_ids, slice) else root_asset_cfg.body_ids + + target_pos = target_asset.data.body_link_pos_w[:, target_body_idx].view(-1, 3) + target_quat = target_asset.data.body_link_quat_w[:, target_body_idx].view(-1, 4) + root_pos = root_asset.data.body_link_pos_w[:, root_body_idx].view(-1, 3) + root_quat = root_asset.data.body_link_quat_w[:, root_body_idx].view(-1, 4) + + if root_asset_offset is not None: + root_pos, root_quat = root_asset_offset.combine(root_pos, root_quat) + if target_asset_offset is not None: + target_pos, target_quat = target_asset_offset.combine(target_pos, target_quat) + + target_pos_b, target_quat_b = math_utils.subtract_frame_transforms(root_pos, root_quat, target_pos, target_quat) + + if rotation_repr == "axis_angle": + axis_angle = math_utils.axis_angle_from_quat(target_quat_b) + return torch.cat([target_pos_b, axis_angle], dim=1) + elif rotation_repr == "quat": + return torch.cat([target_pos_b, target_quat_b], dim=1) + else: + raise ValueError(f"Invalid rotation_repr: {rotation_repr}. Must be one of: 'quat', 'axis_angle'") + + +class target_asset_pose_in_root_asset_frame_with_metadata(ManagerTermBase): + """Get target asset pose in root asset frame with offsets automatically read from metadata. + + This is similar to target_asset_pose_in_root_asset_frame but automatically reads the + assembled offsets from the asset USD metadata instead of requiring manual specification. + """ + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + target_asset_cfg: SceneEntityCfg = cfg.params.get("target_asset_cfg") + root_asset_cfg: SceneEntityCfg = cfg.params.get("root_asset_cfg", SceneEntityCfg("robot")) + target_asset_offset_metadata_key: str = cfg.params.get("target_asset_offset_metadata_key") + root_asset_offset_metadata_key: str = cfg.params.get("root_asset_offset_metadata_key") + + self.target_asset: RigidObject | Articulation = env.scene[target_asset_cfg.name] + self.root_asset: RigidObject | Articulation = env.scene[root_asset_cfg.name] + self.target_asset_cfg = target_asset_cfg + self.root_asset_cfg = root_asset_cfg + self.rotation_repr = cfg.params.get("rotation_repr", "quat") + + # Read root asset offset from metadata + if root_asset_offset_metadata_key is not None: + root_usd_path = self.root_asset.cfg.spawn.usd_path + root_metadata = utils.read_metadata_from_usd_directory(root_usd_path) + root_offset_data = root_metadata.get(root_asset_offset_metadata_key) + self.root_asset_offset = Offset(pos=root_offset_data.get("pos"), quat=root_offset_data.get("quat")) + else: + self.root_asset_offset = None + + # Read target asset offset from metadata + if target_asset_offset_metadata_key is not None: + target_usd_path = self.target_asset.cfg.spawn.usd_path + target_metadata = utils.read_metadata_from_usd_directory(target_usd_path) + target_offset_data = target_metadata.get(target_asset_offset_metadata_key) + self.target_asset_offset = Offset(pos=target_offset_data.get("pos"), quat=target_offset_data.get("quat")) + else: + self.target_asset_offset = None + + def __call__( + self, + env: ManagerBasedEnv, + target_asset_cfg: SceneEntityCfg, + root_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + target_asset_offset_metadata_key: str | None = None, + root_asset_offset_metadata_key: str | None = None, + rotation_repr: str = "quat", + ) -> torch.Tensor: + target_body_idx = 0 if isinstance(self.target_asset_cfg.body_ids, slice) else self.target_asset_cfg.body_ids + root_body_idx = 0 if isinstance(self.root_asset_cfg.body_ids, slice) else self.root_asset_cfg.body_ids + + target_pos = self.target_asset.data.body_link_pos_w[:, target_body_idx].view(-1, 3) + target_quat = self.target_asset.data.body_link_quat_w[:, target_body_idx].view(-1, 4) + root_pos = self.root_asset.data.body_link_pos_w[:, root_body_idx].view(-1, 3) + root_quat = self.root_asset.data.body_link_quat_w[:, root_body_idx].view(-1, 4) + + if self.root_asset_offset is not None: + root_pos, root_quat = self.root_asset_offset.combine(root_pos, root_quat) + if self.target_asset_offset is not None: + target_pos, target_quat = self.target_asset_offset.combine(target_pos, target_quat) + + target_pos_b, target_quat_b = math_utils.subtract_frame_transforms(root_pos, root_quat, target_pos, target_quat) + + if rotation_repr == "axis_angle": + axis_angle = math_utils.axis_angle_from_quat(target_quat_b) + return torch.cat([target_pos_b, axis_angle], dim=1) + elif rotation_repr == "quat": + return torch.cat([target_pos_b, target_quat_b], dim=1) + else: + raise ValueError(f"Invalid rotation_repr: {rotation_repr}. Must be one of: 'quat', 'axis_angle'") + + +def asset_link_velocity_in_root_asset_frame( + env: ManagerBasedEnv, + target_asset_cfg: SceneEntityCfg, + root_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + target_asset: RigidObject | Articulation = env.scene[target_asset_cfg.name] + root_asset: RigidObject | Articulation = env.scene[root_asset_cfg.name] + + taget_body_idx = 0 if isinstance(target_asset_cfg.body_ids, slice) else target_asset_cfg.body_ids + + asset_lin_vel_b, _ = math_utils.subtract_frame_transforms( + root_asset.data.root_pos_w, + root_asset.data.root_quat_w, + target_asset.data.body_lin_vel_w[:, taget_body_idx].view(-1, 3), + ) + asset_ang_vel_b, _ = math_utils.subtract_frame_transforms( + root_asset.data.root_pos_w, + root_asset.data.root_quat_w, + target_asset.data.body_lin_vel_w[:, taget_body_idx].view(-1, 3), + ) + + return torch.cat([asset_lin_vel_b, asset_ang_vel_b], dim=1) + + +def get_material_properties( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg, +): + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + return asset.root_physx_view.get_material_properties().view(env.num_envs, -1) + + +def get_mass( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg, +): + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + return asset.root_physx_view.get_masses().view(env.num_envs, -1) + + +def get_joint_friction( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg, +): + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + return asset.data.joint_friction_coeff.view(env.num_envs, -1) + + +def get_joint_armature( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg, +): + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + return asset.data.joint_armature.view(env.num_envs, -1) + + +def get_joint_stiffness( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg, +): + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + return asset.data.joint_stiffness.view(env.num_envs, -1) + + +def get_joint_damping( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg, +): + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + return asset.data.joint_damping.view(env.num_envs, -1) + + +def time_left(env) -> torch.Tensor: + if hasattr(env, "episode_length_buf"): + life_left = 1 - (env.episode_length_buf.float() / env.max_episode_length) + else: + life_left = torch.zeros(env.num_envs, device=env.device, dtype=torch.float) + return life_left.view(-1, 1) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/__init__.py new file mode 100644 index 0000000..9e8421d --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Various recorder terms that can be used in the environment.""" + +from .recorders import * +from .recorders_cfg import * diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/recorders.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/recorders.py new file mode 100644 index 0000000..37c5049 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/recorders.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from __future__ import annotations + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers.recorder_manager import RecorderTerm + + +class StableStateRecorder(RecorderTerm): + def record_pre_reset(self, env_ids): + def extract_env_ids_values(value): + nonlocal env_ids + if isinstance(value, dict): + return {k: extract_env_ids_values(v) for k, v in value.items()} + return value[env_ids] + + return "initial_state", extract_env_ids_values(self._env.scene.get_state(is_relative=True)) + + +class GraspRelativePoseRecorder(RecorderTerm): + """Recorder term that records relative position, orientation, and gripper joint states for grasp evaluation.""" + + def __init__(self, cfg, env): + super().__init__(cfg, env) + # Configuration for which robot and object to track + self.robot_name = cfg.robot_name + self.object_name = cfg.object_name + self.gripper_body_name = cfg.gripper_body_name + + def record_pre_reset(self, env_ids): + """Record relative pose between object and gripper, plus gripper joint states before reset.""" + if env_ids is None: + env_ids = torch.arange(self._env.num_envs, device=self._env.device) + elif not isinstance(env_ids, torch.Tensor): + env_ids = torch.tensor(env_ids, device=self._env.device) + + # Get robot articulation and object rigid body + robot = self._env.scene[self.robot_name] + obj = self._env.scene[self.object_name] + + # Get object pose (root pose contains position and orientation) + obj_root_state = obj.data.root_state_w[env_ids] # Shape: (num_envs, 13) - pos(3) + quat(4) + vel(6) + obj_pos = obj_root_state[:, :3] # Position + obj_quat = obj_root_state[:, 3:7] # Quaternion (w, x, y, z) + + # Get gripper body pose from the robot articulation + # Find the gripper body index + gripper_body_idx = None + for idx, body_name in enumerate(robot.body_names): + if self.gripper_body_name in body_name: + gripper_body_idx = idx + break + + # Get specific body pose + gripper_pos = robot.data.body_state_w[env_ids, gripper_body_idx, :3] + gripper_quat = robot.data.body_state_w[env_ids, gripper_body_idx, 3:7] + + # Calculate relative transform: T_gripper_in_object = T_object^{-1} * T_gripper + relative_pos, relative_quat = math_utils.subtract_frame_transforms(obj_pos, obj_quat, gripper_pos, gripper_quat) + + # Get gripper joint states as dict mapping joint names to positions + gripper_joint_pos = robot.data.joint_pos[env_ids].clone() + gripper_joint_dict = {joint_name: gripper_joint_pos[:, i] for i, joint_name in enumerate(robot.joint_names)} + + # Prepare data to record + grasp_data = { + "relative_position": relative_pos, + "relative_orientation": relative_quat, + "gripper_joint_positions": gripper_joint_dict, + } + + return "grasp_relative_pose", grasp_data diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/recorders_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/recorders_cfg.py new file mode 100644 index 0000000..ad6fb25 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/recorders/recorders_cfg.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from isaaclab.managers.recorder_manager import RecorderManagerBaseCfg, RecorderTerm, RecorderTermCfg +from isaaclab.utils import configclass + +from . import recorders + +## +# State recorders. +## + + +# Stable state recorder. +@configclass +class StableStateRecorderCfg(RecorderTermCfg): + """Configuration for the initial state recorder term.""" + + class_type: type[RecorderTerm] = recorders.StableStateRecorder + + +@configclass +class StableStateRecorderManagerCfg(RecorderManagerBaseCfg): + """Recorder configurations for recording actions and states.""" + + record_pre_reset_states = StableStateRecorderCfg() + + +# Grasp relative pose recorder. +@configclass +class GraspRelativePoseRecorderCfg(RecorderTermCfg): + """Configuration for the grasp relative pose recorder term.""" + + class_type: type[RecorderTerm] = recorders.GraspRelativePoseRecorder + robot_name: str = MISSING + object_name: str = MISSING + gripper_body_name: str = MISSING + + +@configclass +class GraspRelativePoseRecorderManagerCfg(RecorderManagerBaseCfg): + """Configuration for the grasp relative pose recorder manager.""" + + def __init__(self, *args, **kwargs): + """Initialize with explicit parameters for cleaner interface. + + Args: + robot_name: Name of the robot in the scene to track. + object_name: Name of the object in the scene to track. + gripper_body_name: Name of the gripper body for pose tracking. + **kwargs: Additional arguments passed to parent class. + """ + robot_name = kwargs.pop("robot_name", MISSING) + object_name = kwargs.pop("object_name", MISSING) + gripper_body_name = kwargs.pop("gripper_body_name", MISSING) + super().__init__(args, **kwargs) + self.record_grasp_relative_pose = GraspRelativePoseRecorderCfg( + robot_name=robot_name, + object_name=object_name, + gripper_body_name=gripper_body_name, + ) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/rewards.py new file mode 100644 index 0000000..1a3ef15 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/rewards.py @@ -0,0 +1,217 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import ManagerTermBase, RewardTermCfg, SceneEntityCfg + +from ..assembly_keypoints import Offset +from . import utils +from .collision_analyzer_cfg import CollisionAnalyzerCfg +from .success_monitor_cfg import SuccessMonitorCfg + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + from .commands import TaskCommand + + +class ee_asset_distance_tanh(ManagerTermBase): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + self.root_asset_cfg = cfg.params.get("root_asset_cfg") + self.target_asset_cfg = cfg.params.get("target_asset_cfg") + self.std = cfg.params.get("std") + + root_asset_offset_metadata_key: str = cfg.params.get("root_asset_offset_metadata_key") + target_asset_offset_metadata_key: str = cfg.params.get("target_asset_offset_metadata_key") + + self.root_asset = env.scene[self.root_asset_cfg.name] + root_usd_path = self.root_asset.cfg.spawn.usd_path + root_metadata = utils.read_metadata_from_usd_directory(root_usd_path) + root_offset_data = root_metadata.get(root_asset_offset_metadata_key) + self.root_asset_offset = Offset(pos=root_offset_data.get("pos"), quat=root_offset_data.get("quat")) + + self.target_asset = env.scene[self.target_asset_cfg.name] + if target_asset_offset_metadata_key is not None: + target_usd_path = self.target_asset.cfg.spawn.usd_path + target_metadata = utils.read_metadata_from_usd_directory(target_usd_path) + target_offset_data = target_metadata.get(target_asset_offset_metadata_key) + self.target_asset_offset = Offset(pos=target_offset_data.get("pos"), quat=target_offset_data.get("quat")) + else: + self.target_asset_offset = None + + def __call__( + self, + env: ManagerBasedRLEnv, + root_asset_cfg: SceneEntityCfg, + target_asset_cfg: SceneEntityCfg, + root_asset_offset_metadata_key: str, + target_asset_offset_metadata_key: str | None = None, + std: float = 0.1, + ) -> torch.Tensor: + root_asset_alignment_pos_w, root_asset_alignment_quat_w = self.root_asset_offset.combine( + self.root_asset.data.body_link_pos_w[:, root_asset_cfg.body_ids].view(-1, 3), + self.root_asset.data.body_link_quat_w[:, root_asset_cfg.body_ids].view(-1, 4), + ) + if self.target_asset_offset is None: + target_asset_alignment_pos_w = self.target_asset.data.root_pos_w.view(-1, 3) + target_asset_alignment_quat_w = self.target_asset.data.root_quat_w.view(-1, 4) + else: + target_asset_alignment_pos_w, target_asset_alignment_quat_w = self.target_asset_offset.apply( + self.target_asset + ) + target_asset_in_root_asset_frame_pos, target_asset_in_root_asset_frame_angle_axis = ( + math_utils.compute_pose_error( + root_asset_alignment_pos_w, + root_asset_alignment_quat_w, + target_asset_alignment_pos_w, + target_asset_alignment_quat_w, + ) + ) + + pos_distance = torch.norm(target_asset_in_root_asset_frame_pos, dim=1) + + return 1 - torch.tanh(pos_distance / std) + + +class ProgressContext(ManagerTermBase): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + self.insertive_asset: Articulation | RigidObject = env.scene[cfg.params.get("insertive_asset_cfg").name] # type: ignore + self.receptive_asset: Articulation | RigidObject = env.scene[cfg.params.get("receptive_asset_cfg").name] # type: ignore + + insertive_meta = utils.read_metadata_from_usd_directory(self.insertive_asset.cfg.spawn.usd_path) + receptive_meta = utils.read_metadata_from_usd_directory(self.receptive_asset.cfg.spawn.usd_path) + self.insertive_asset_offset = Offset( + pos=tuple(insertive_meta.get("assembled_offset").get("pos")), + quat=tuple(insertive_meta.get("assembled_offset").get("quat")), + ) + self.receptive_asset_offset = Offset( + pos=tuple(receptive_meta.get("assembled_offset").get("pos")), + quat=tuple(receptive_meta.get("assembled_offset").get("quat")), + ) + + self.orientation_aligned = torch.zeros((env.num_envs), dtype=torch.bool, device=env.device) + self.position_aligned = torch.zeros((env.num_envs), dtype=torch.bool, device=env.device) + self.euler_xy_distance = torch.zeros((env.num_envs), device=env.device) + self.xyz_distance = torch.zeros((env.num_envs), device=env.device) + self.success = torch.zeros((self._env.num_envs), dtype=torch.bool, device=self._env.device) + self.continuous_success_counter = torch.zeros((self._env.num_envs), dtype=torch.int32, device=self._env.device) + + success_monitor_cfg = SuccessMonitorCfg(monitored_history_len=100, num_monitored_data=1, device=env.device) + self.success_monitor = success_monitor_cfg.class_type(success_monitor_cfg) + + def reset(self, env_ids: torch.Tensor | None = None) -> None: + super().reset(env_ids) + self.continuous_success_counter[:] = 0 + + def __call__( + self, + env: ManagerBasedRLEnv, + insertive_asset_cfg: SceneEntityCfg, + receptive_asset_cfg: SceneEntityCfg, + command_context: str = "task_command", + ) -> torch.Tensor: + task_command: TaskCommand = env.command_manager.get_term(command_context) + success_position_threshold = task_command.success_position_threshold + success_orientation_threshold = task_command.success_orientation_threshold + insertive_asset_alignment_pos_w, insertive_asset_alignment_quat_w = self.insertive_asset_offset.apply( + self.insertive_asset + ) + receptive_asset_alignment_pos_w, receptive_asset_alignment_quat_w = self.receptive_asset_offset.apply( + self.receptive_asset + ) + insertive_asset_in_receptive_asset_frame_pos, insertive_asset_in_receptive_asset_frame_quat = ( + math_utils.subtract_frame_transforms( + receptive_asset_alignment_pos_w, + receptive_asset_alignment_quat_w, + insertive_asset_alignment_pos_w, + insertive_asset_alignment_quat_w, + ) + ) + # yaw could be different + e_x, e_y, _ = math_utils.euler_xyz_from_quat(insertive_asset_in_receptive_asset_frame_quat) + self.euler_xy_distance[:] = math_utils.wrap_to_pi(e_x).abs() + math_utils.wrap_to_pi(e_y).abs() + self.xyz_distance[:] = torch.norm(insertive_asset_in_receptive_asset_frame_pos, dim=1) + self.position_aligned[:] = self.xyz_distance < success_position_threshold + self.orientation_aligned[:] = self.euler_xy_distance < success_orientation_threshold + self.success[:] = self.orientation_aligned & self.position_aligned + + # Update continuous success counter + self.continuous_success_counter[:] = torch.where( + self.success, self.continuous_success_counter + 1, torch.zeros_like(self.continuous_success_counter) + ) + + # Update success monitor + self.success_monitor.success_update( + torch.zeros(env.num_envs, dtype=torch.int32, device=env.device), self.success + ) + + return torch.zeros(env.num_envs, device=env.device) + + +def dense_success_reward(env: ManagerBasedRLEnv, std: float, context: str = "progress_context") -> torch.Tensor: + + context_term: ManagerTermBase = env.reward_manager.get_term_cfg(context).func # type: ignore + angle_diff: torch.Tensor = getattr(context_term, "euler_xy_distance") + xyz_distance: torch.Tensor = getattr(context_term, "xyz_distance") + + # Normalize the distances by std + angle_diff = torch.exp(-angle_diff / std) + xyz_distance = torch.exp(-xyz_distance / std) + stacked = torch.stack([angle_diff, xyz_distance], dim=0) + return torch.mean(stacked, dim=0) + + +def success_reward(env: ManagerBasedRLEnv, context: str = "progress_context") -> torch.Tensor: + context_term: ManagerTermBase = env.reward_manager.get_term_cfg(context).func # type: ignore + orientation_aligned: torch.Tensor = getattr(context_term, "orientation_aligned") + position_aligned: torch.Tensor = getattr(context_term, "position_aligned") + return torch.where(orientation_aligned & position_aligned, 1.0, 0.0) + + +def action_l2_clamped(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize the actions using L2 squared kernel.""" + return torch.clamp(torch.sum(torch.square(env.action_manager.action), dim=1), 0, 1e4) + + +def action_rate_l2_clamped(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize the rate of change of the actions using L2 squared kernel.""" + return torch.clamp( + torch.sum(torch.square(env.action_manager.action - env.action_manager.prev_action), dim=1), 0, 1e4 + ) + + +def joint_vel_l2_clamped(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. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return torch.clamp(torch.sum(torch.square(asset.data.joint_vel[:, asset_cfg.joint_ids]), dim=1), 0, 1e4) + + +class collision_free(ManagerTermBase): + def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + + self._env = env + + self.collision_analyzer_cfg = cfg.params.get("collision_analyzer_cfg") + self.collision_analyzer = self.collision_analyzer_cfg.class_type(self.collision_analyzer_cfg, self._env) + + def __call__(self, env: ManagerBasedRLEnv, collision_analyzer_cfg: CollisionAnalyzerCfg) -> torch.Tensor: + all_env_ids = torch.arange(env.num_envs, device=env.device) + collision_free = self.collision_analyzer(env, all_env_ids) + + return collision_free diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/rigid_object_hasher.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/rigid_object_hasher.py new file mode 100644 index 0000000..da2b517 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/rigid_object_hasher.py @@ -0,0 +1,177 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import hashlib +import numpy as np +import torch + +import isaacsim.core.utils.prims as prim_utils +import isaacsim.core.utils.stage as stage_utils +import warp as wp +from isaaclab.sim import get_all_matching_child_prims +from pxr import Gf, Usd, UsdGeom, UsdPhysics + +HASH_STORE = {"warp_mesh_store": {}, "__stage_id__": None} + + +class RigidObjectHasher: + """Compute per-root and per-collider 64-bit hashes of transform+geometry.""" + + def __init__(self, num_envs, prim_path_pattern, device="cpu"): + self.prim_path_pattern = prim_path_pattern + self.device = device + # Invalidate cache if USD stage changed between runs (minimal, self-contained). + stage_id = stage_utils.get_current_stage_id() + prev_stage_id = HASH_STORE.get("__stage_id__") + if prev_stage_id is not None and stage_id is not None and prev_stage_id != stage_id: + HASH_STORE.clear() + HASH_STORE["warp_mesh_store"] = {} + HASH_STORE["__stage_id__"] = stage_id + elif prev_stage_id is None and stage_id is not None: + HASH_STORE["__stage_id__"] = stage_id + + if prim_path_pattern in HASH_STORE: + return + + HASH_STORE[prim_path_pattern] = { + "num_roots": 0, + "collider_prims": [], + "collider_prim_hashes": [], + "collider_prim_env_ids": [], + "collider_prim_relative_transforms": [], + "root_prim_hashes": [], + "root_prim_scales": [], + } + stor = HASH_STORE[prim_path_pattern] + xform_cache = UsdGeom.XformCache() + prim_paths = [prim_path_pattern.replace(".*", f"{i}", 1) for i in range(num_envs)] + + num_roots = len(prim_paths) + collider_prim_env_ids = [] + collider_prims: list[Usd.Prim] = [] + collider_prim_relative_transforms = [] + collider_prim_hashes = [] + root_prim_hashes = [] + root_prim_scales = [] + for i in range(num_roots): + # 1: Get all child prims that are colliders, count them, and store their belonging env id + coll_prims = get_all_matching_child_prims( + prim_paths[i], + predicate=lambda p: p.GetTypeName() in ("Mesh", "Cube", "Sphere", "Cylinder", "Capsule", "Cone") + and p.HasAPI(UsdPhysics.CollisionAPI), + traverse_instance_prims=True, + ) + if len(coll_prims) == 0: + return + collider_prims.extend(coll_prims) + collider_prim_env_ids.extend([i] * len(coll_prims)) + + # 2: Get relative transforms of all collider prims + root_xf = xform_cache.GetLocalToWorldTransform(prim_utils.get_prim_at_path(prim_paths[i])) + root_tf = Gf.Transform(root_xf) + rel_tfs = [] + root_prim_scales.append(torch.tensor(root_tf.GetScale())) + for prim in coll_prims: + child_xf = xform_cache.GetLocalToWorldTransform(prim) + rel_mat_tf = Gf.Transform(child_xf * root_xf.GetInverse()) + rel_quat = rel_mat_tf.GetRotation().GetQuat() + rel_t = torch.tensor(rel_mat_tf.GetTranslation()) + rel_q = torch.tensor([rel_quat.GetReal(), *rel_quat.GetImaginary()]) + rel_s = torch.tensor(rel_mat_tf.GetScale()) + rel_tfs.append(torch.cat([rel_t, rel_q, rel_s])) + rel_tfs = torch.cat(rel_tfs) + collider_prim_relative_transforms.append(rel_tfs) + + # 3: Store the collider prims hash + root_hash = hashlib.sha256() + for prim, prim_rel_tf in zip(coll_prims, rel_tfs.numpy()): + h = hashlib.sha256() + h.update( + np.round(prim_rel_tf * 50).astype(np.int64) + ) # round so small, +-2cm tol, difference won't cause issue + prim_type = prim.GetTypeName() + h.update(prim_type.encode("utf-8")) + if prim_type == "Mesh": + verts = np.asarray(UsdGeom.Mesh(prim).GetPointsAttr().Get(), dtype=np.float32) + h.update(verts.tobytes()) + else: + if prim_type == "Cube": + s = UsdGeom.Cube(prim).GetSizeAttr().Get() + h.update(np.float32(s).tobytes()) + elif prim_type == "Sphere": + r = UsdGeom.Sphere(prim).GetRadiusAttr().Get() + h.update(np.float32(r).tobytes()) + elif prim_type == "Cylinder": + c = UsdGeom.Cylinder(prim) + h.update(np.float32(c.GetRadiusAttr().Get()).tobytes()) + h.update(np.float32(c.GetHeightAttr().Get()).tobytes()) + elif prim_type == "Capsule": + c = UsdGeom.Capsule(prim) + h.update(c.GetAxisAttr().Get().encode("utf-8")) + h.update(np.float32(c.GetRadiusAttr().Get()).tobytes()) + h.update(np.float32(c.GetHeightAttr().Get()).tobytes()) + elif prim_type == "Cone": + c = UsdGeom.Cone(prim) + h.update(np.float32(c.GetRadiusAttr().Get()).tobytes()) + h.update(np.float32(c.GetHeightAttr().Get()).tobytes()) + collider_hash = h.digest() + root_hash.update(collider_hash) + collider_prim_hashes.append(int.from_bytes(collider_hash[:8], "little", signed=True)) + small = int.from_bytes(root_hash.digest()[:8], "little", signed=True) + root_prim_hashes.append(small) + + stor["num_roots"] = num_roots + stor["collider_prims"] = collider_prims + stor["collider_prim_hashes"] = torch.tensor(collider_prim_hashes, dtype=torch.int64, device="cpu") + stor["collider_prim_env_ids"] = torch.tensor(collider_prim_env_ids, dtype=torch.int64, device="cpu") + stor["collider_prim_relative_transforms"] = torch.cat(collider_prim_relative_transforms).view(-1, 10).to("cpu") + stor["root_prim_hashes"] = torch.tensor(root_prim_hashes, dtype=torch.int64, device="cpu") + stor["root_prim_scales"] = torch.stack(root_prim_scales).to("cpu") + + @property + def num_root(self) -> int: + return self.get_val("num_roots") + + @property + def root_prim_hashes(self) -> torch.Tensor: + return self.get_val("root_prim_hashes").to(self.device) + + @property + def root_prim_scales(self) -> torch.Tensor: + """Get the root prim transforms.""" + return self.get_val("root_prim_scales").to(self.device) + + @property + def collider_prim_relative_transforms(self) -> torch.Tensor: + return self.get_val("collider_prim_relative_transforms").to(self.device) + + @property + def collider_prim_hashes(self) -> torch.Tensor: + return self.get_val("collider_prim_hashes").to(self.device) + + @property + def collider_prims(self) -> list[Usd.Prim]: + return self.get_val("collider_prims") + + @property + def collider_prim_env_ids(self) -> torch.Tensor: + return self.get_val("collider_prim_env_ids").to(self.device) + + def get_val(self, key: str): + """Get the hash store for the hasher.""" + return HASH_STORE.get(self.prim_path_pattern, {}).get(key) + + def set_val(self, key: str, val: any): + if isinstance(val, torch.Tensor): + val = val.to("cpu") + HASH_STORE[self.prim_path_pattern][key] = val + + def get_warp_mesh_store(self) -> dict[int, wp.Mesh]: + """Get the warp mesh store for the hasher.""" + return HASH_STORE["warp_mesh_store"] + + def get_hash_store(self) -> dict[int, any]: + """Get the entire hash store""" + return HASH_STORE diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/success_monitor.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/success_monitor.py new file mode 100644 index 0000000..41d72af --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/success_monitor.py @@ -0,0 +1,54 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .success_monitor_cfg import SuccessMonitorCfg + + +class SuccessMonitor: + def __init__(self, cfg: SuccessMonitorCfg): + + # uniform success buff + self.monitored_history_len = cfg.monitored_history_len + self.device = cfg.device + self.success_buf = torch.zeros((cfg.num_monitored_data, self.monitored_history_len), device=self.device) + self.success_rate = torch.zeros((cfg.num_monitored_data), device=self.device) + self.success_pointer = torch.zeros((cfg.num_monitored_data), device=self.device, dtype=torch.int32) + self.success_size = torch.zeros((cfg.num_monitored_data), device=self.device, dtype=torch.int32) + + def failure_rate_sampling(self, env_ids): + failure_rate = (1 - self.success_rate).clamp(min=1e-6) + return torch.multinomial(failure_rate.view(-1), len(env_ids), replacement=True).to(torch.int32) + + def success_update(self, ids_all, success_mask): + unique_indices, inv, counts = torch.unique(ids_all, return_inverse=True, return_counts=True) + counts_clamped = counts.clamp(max=self.monitored_history_len).to(dtype=self.success_pointer.dtype) + + ptrs = self.success_pointer[unique_indices] + values = (success_mask[torch.argsort(inv)]).to(device=self.device, dtype=self.success_buf.dtype) + values_splits = torch.split(values, counts.tolist()) + clamped_values = torch.cat([grp[-n:] for grp, n in zip(values_splits, counts_clamped.tolist())]) + state_indices = torch.repeat_interleave(unique_indices, counts_clamped) + buf_indices = torch.cat([ + torch.arange(start, start + n, dtype=torch.int64, device=self.device) % self.monitored_history_len + for start, n in zip(ptrs.tolist(), counts_clamped.tolist()) + ]) + + self.success_buf.index_put_((state_indices, buf_indices), clamped_values) + + self.success_pointer.index_add_(0, unique_indices, counts_clamped) + self.success_pointer = self.success_pointer % self.monitored_history_len + + self.success_size.index_add_(0, unique_indices, counts_clamped) + self.success_size = self.success_size.clamp(max=self.monitored_history_len) + self.success_rate[:] = self.success_buf.sum(dim=1) / self.success_size.clamp(min=1) + + def get_success_rate(self): + return self.success_rate.clone() diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/success_monitor_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/success_monitor_cfg.py new file mode 100644 index 0000000..f61fc5a --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/success_monitor_cfg.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from dataclasses import MISSING + +from isaaclab.utils import configclass + +from .success_monitor import SuccessMonitor + + +@configclass +class SuccessMonitorCfg: + + class_type: type[SuccessMonitor] = SuccessMonitor + + monitored_history_len: int = 100 + """The total length of success entry recorded, monitoring table size: (num_monitored_data, monitored_history_len)""" + + num_monitored_data: int = MISSING + """Number of success monitored. monitoring table size: (num_monitored_data, monitored_history_len)""" + + device: str = "cpu" + """The device used to maintain success table data structure""" diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/terminations.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/terminations.py new file mode 100644 index 0000000..c706a4b --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/terminations.py @@ -0,0 +1,651 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""MDP functions for manipulation tasks.""" + +import numpy as np +import torch + +import isaacsim.core.utils.bounds as bounds_utils +from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection +from isaaclab.envs import ManagerBasedEnv +from isaaclab.managers import ManagerTermBase, SceneEntityCfg, TerminationTermCfg +from isaaclab.utils import math as math_utils + +from uwlab_tasks.manager_based.manipulation.reset_states.mdp import utils + +from .collision_analyzer_cfg import CollisionAnalyzerCfg + + +def _check_obb_overlap(centroids_a, axes_a, half_extents_a, centroids_b, axes_b, half_extents_b) -> torch.Tensor: + """ + OBB overlap check. + + Args: + centroids_a: Centers of OBB A for all envs (num_envs, 3) - torch tensor on GPU + axes_a: Orientation axes of OBB A for all envs (num_envs, 3, 3) - torch tensor on GPU + half_extents_a: Half extents of OBB A (3,) - torch tensor on GPU + centroids_b: Centers of OBB B for all envs (num_envs, 3) - torch tensor on GPU + axes_b: Orientation axes of OBB B for all envs (num_envs, 3, 3) - torch tensor on GPU + half_extents_b: Half extents of OBB B (3,) - torch tensor on GPU + + Returns: + torch.Tensor: Boolean tensor (num_envs,) indicating overlap for each environment + """ + num_envs = centroids_a.shape[0] + device = centroids_a.device + + # Vector between centroids for all envs (num_envs, 3) + d = centroids_b - centroids_a + + # Matrix C = A^T * B (rotation from A to B) for all envs (num_envs, 3, 3) + C = torch.bmm(axes_a.transpose(1, 2), axes_b) + abs_C = torch.abs(C) + + # Initialize overlap results (assume all overlap initially) + overlap_results = torch.ones(num_envs, device=device, dtype=torch.bool) + + # Test all axes of A at once (vectorized across all 3 axes and all environments) + # axes_a: (num_envs, 3, 3), d: (num_envs, 3) -> projections: (num_envs, 3) + projections_on_axes_a = torch.abs(torch.bmm(d.unsqueeze(1), axes_a).squeeze(1)) # (num_envs, 3) + ra_all = half_extents_a.unsqueeze(0).expand(num_envs, -1) # (num_envs, 3) + rb_all = torch.sum(half_extents_b.unsqueeze(0).unsqueeze(0) * abs_C, dim=2) # (num_envs, 3) + no_overlap_a = projections_on_axes_a > (ra_all + rb_all) # (num_envs, 3) + overlap_results &= ~torch.any(no_overlap_a, dim=1) # (num_envs,) + + # Test all axes of B at once (vectorized across all 3 axes and all environments) + # axes_b: (num_envs, 3, 3), d: (num_envs, 3) -> projections: (num_envs, 3) + projections_on_axes_b = torch.abs(torch.bmm(d.unsqueeze(1), axes_b).squeeze(1)) # (num_envs, 3) + ra_all_b = torch.sum(half_extents_a.unsqueeze(0).unsqueeze(0) * abs_C.transpose(1, 2), dim=2) # (num_envs, 3) + rb_all_b = half_extents_b.unsqueeze(0).expand(num_envs, -1) # (num_envs, 3) + no_overlap_b = projections_on_axes_b > (ra_all_b + rb_all_b) # (num_envs, 3) + overlap_results &= ~torch.any(no_overlap_b, dim=1) # (num_envs,) + + # Test all cross products at once (9 cross products per environment) + # Reshape axes for broadcasting: axes_a (num_envs, 3, 1, 3), axes_b (num_envs, 1, 3, 3) + axes_a_expanded = axes_a.unsqueeze(2) # (num_envs, 3, 1, 3) + axes_b_expanded = axes_b.unsqueeze(1) # (num_envs, 1, 3, 3) + + # Compute all 9 cross products at once: (num_envs, 3, 3, 3) + cross_products = torch.cross(axes_a_expanded, axes_b_expanded, dim=3) # (num_envs, 3, 3, 3) + + # Compute norms and filter out near-parallel axes: (num_envs, 3, 3) + cross_norms = torch.norm(cross_products, dim=3) # (num_envs, 3, 3) + valid_crosses = cross_norms > 1e-6 # (num_envs, 3, 3) + + # Normalize cross products (set invalid ones to zero) + normalized_crosses = torch.where( + valid_crosses.unsqueeze(3), + cross_products / cross_norms.unsqueeze(3).clamp(min=1e-6), + torch.zeros_like(cross_products), + ) # (num_envs, 3, 3, 3) + + # Project d onto all cross product axes: (num_envs, 3, 3) + d_expanded = d.unsqueeze(1).unsqueeze(1) # (num_envs, 1, 1, 3) + projections_cross = torch.abs(torch.sum(d_expanded * normalized_crosses, dim=3)) # (num_envs, 3, 3) + + # Compute ra for all cross products: (num_envs, 3, 3) + # half_extents_a: (3,), axes_a: (num_envs, 3, 3), normalized_crosses: (num_envs, 3, 3, 3) + axes_a_cross_dots = torch.abs( + torch.sum(axes_a.unsqueeze(1).unsqueeze(1) * normalized_crosses.unsqueeze(3), dim=4) + ) # (num_envs, 3, 3, 3) + ra_cross = torch.sum( + half_extents_a.unsqueeze(0).unsqueeze(0).unsqueeze(0) * axes_a_cross_dots, dim=3 + ) # (num_envs, 3, 3) + + # Compute rb for all cross products: (num_envs, 3, 3) + axes_b_cross_dots = torch.abs( + torch.sum(axes_b.unsqueeze(1).unsqueeze(1) * normalized_crosses.unsqueeze(4), dim=4) + ) # (num_envs, 3, 3, 3) + rb_cross = torch.sum( + half_extents_b.unsqueeze(0).unsqueeze(0).unsqueeze(0) * axes_b_cross_dots, dim=3 + ) # (num_envs, 3, 3) + + # Check separating condition for all cross products: (num_envs, 3, 3) + no_overlap_cross = projections_cross > (ra_cross + rb_cross) # (num_envs, 3, 3) + # Only consider valid cross products + no_overlap_cross_valid = no_overlap_cross & valid_crosses # (num_envs, 3, 3) + overlap_results &= ~torch.any(no_overlap_cross_valid.view(num_envs, -1), dim=1) # (num_envs,) + + return overlap_results + + +class check_grasp_success(ManagerTermBase): + """Check if grasp is successful based on object stability, gripper closure, and collision detection.""" + + def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + self._env = env + + self.object_cfg = cfg.params.get("object_cfg") + self.gripper_cfg = cfg.params.get("gripper_cfg") + self.collision_analyzer_cfg = cfg.params.get("collision_analyzer_cfg") + self.collision_analyzer = self.collision_analyzer_cfg.class_type(self.collision_analyzer_cfg, self._env) + self.max_pos_deviation = cfg.params.get("max_pos_deviation") + self.pos_z_threshold = cfg.params.get("pos_z_threshold") + + def reset(self, env_ids: torch.Tensor | None = None) -> None: + super().reset(env_ids) + + object_asset = self._env.scene[self.object_cfg.name] + if not hasattr(object_asset, "initial_pos"): + object_asset.initial_pos = object_asset.data.root_pos_w.clone() + object_asset.initial_quat = object_asset.data.root_quat_w.clone() + else: + object_asset.initial_pos[env_ids] = object_asset.data.root_pos_w[env_ids].clone() + object_asset.initial_quat[env_ids] = object_asset.data.root_quat_w[env_ids].clone() + + def __call__( + self, + env: ManagerBasedEnv, + object_cfg: SceneEntityCfg, + gripper_cfg: SceneEntityCfg, + collision_analyzer_cfg: CollisionAnalyzerCfg, + max_pos_deviation: float = 0.05, + pos_z_threshold: float = 0.05, + ) -> torch.Tensor: + # Get object and gripper from scene + object_asset = env.scene[self.object_cfg.name] + gripper_asset = env.scene[self.gripper_cfg.name] + + # Check time out + time_out = env.episode_length_buf >= env.max_episode_length + + # Check for abnormal gripper state (excessive joint velocities) + abnormal_gripper_state = (gripper_asset.data.joint_vel.abs() > (gripper_asset.data.joint_vel_limits * 2)).any( + dim=1 + ) + + # Skip if position or quaternion is NaN + pos_is_nan = torch.isnan(object_asset.data.root_pos_w).any(dim=1) + quat_is_nan = torch.isnan(object_asset.data.root_quat_w).any(dim=1) + skip_check = pos_is_nan | quat_is_nan + + # Object has excessive pose deviation if position exceeds thresholds + pos_deviation = (object_asset.data.root_pos_w - object_asset.initial_pos).norm(dim=1) + valid_pos_deviation = torch.where(~skip_check, pos_deviation, torch.zeros_like(pos_deviation)) + excessive_pose_deviation = valid_pos_deviation > self.max_pos_deviation + + # Object is above ground if position is greater than z threshold + pos_above_ground = object_asset.data.root_pos_w[:, 2] >= self.pos_z_threshold + + # Check for collisions between gripper and object + all_env_ids = torch.arange(env.num_envs, device=env.device) + collision_free = self.collision_analyzer(env, all_env_ids) + + grasp_success = ( + (~abnormal_gripper_state) & (~excessive_pose_deviation) & pos_above_ground & collision_free & time_out + ) + + return grasp_success + + +class check_reset_state_success(ManagerTermBase): + """Check if grasp is successful based on object stability, gripper closure, and collision detection.""" + + def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + self._env = env + + self.object_cfgs = cfg.params.get("object_cfgs") + self.robot_cfg = cfg.params.get("robot_cfg") + self.ee_body_name = cfg.params.get("ee_body_name") + self.collision_analyzer_cfgs = cfg.params.get("collision_analyzer_cfgs") + self.collision_analyzers = [ + collision_analyzer_cfg.class_type(collision_analyzer_cfg, self._env) + for collision_analyzer_cfg in self.collision_analyzer_cfgs + ] + self.max_robot_pos_deviation = cfg.params.get("max_robot_pos_deviation") + self.max_object_pos_deviation = cfg.params.get("max_object_pos_deviation") + self.pos_z_threshold = cfg.params.get("pos_z_threshold") + self.consecutive_stability_steps = cfg.params.get("consecutive_stability_steps", 5) + + # Load gripper_approach_direction from metadata + robot_asset = env.scene[self.robot_cfg.name] + usd_path = robot_asset.cfg.spawn.usd_path + metadata = utils.read_metadata_from_usd_directory(usd_path) + self.gripper_approach_direction = tuple(metadata.get("gripper_approach_direction")) + + # Initialize stability counter for consecutive stability checking + self.stability_counter = torch.zeros(env.num_envs, device=env.device, dtype=torch.int32) + + self.object_assets = [env.scene[cfg.name] for cfg in self.object_cfgs] + self.robot_asset = env.scene[self.robot_cfg.name] + self.assets_to_check = self.object_assets + [self.robot_asset] + self.ee_body_idx = self.robot_asset.data.body_names.index(self.ee_body_name) + + def reset(self, env_ids: torch.Tensor | None = None) -> None: + super().reset(env_ids) + + for asset in self.assets_to_check: + if asset is self.robot_asset: + asset_pos = asset.data.body_link_pos_w[:, self.ee_body_idx].clone() + else: + asset_pos = asset.data.root_pos_w.clone() + if not hasattr(asset, "initial_pos"): + asset.initial_pos = asset_pos + else: + asset.initial_pos[env_ids] = asset_pos[env_ids].clone() + + self.stability_counter[env_ids] = 0 + + def __call__( + self, + env: ManagerBasedEnv, + object_cfgs: list[SceneEntityCfg], + robot_cfg: SceneEntityCfg, + ee_body_name: str, + collision_analyzer_cfgs: list[CollisionAnalyzerCfg], + max_robot_pos_deviation: float = 0.1, + max_object_pos_deviation: float = 0.1, + pos_z_threshold: float = -0.01, + consecutive_stability_steps: int = 5, + ) -> torch.Tensor: + + # Check time out + time_out = env.episode_length_buf >= env.max_episode_length + + # Check for abnormal gripper state (excessive joint velocities) + abnormal_gripper_state = ( + self.robot_asset.data.joint_vel.abs() > (self.robot_asset.data.joint_vel_limits * 2) + ).any(dim=1) + + # Check if gripper orientation is pointing downward within 60 degrees of vertical + ee_quat = self.robot_asset.data.body_link_quat_w[:, self.ee_body_idx] + gripper_approach_local = torch.tensor( + self.gripper_approach_direction, device=env.device, dtype=torch.float32 + ).expand(env.num_envs, -1) + gripper_approach_world = math_utils.quat_apply(ee_quat, gripper_approach_local) + gripper_orientation_within_range = ( + gripper_approach_world[:, 2] < -0.5 + ) # cos(60Β°) = 0.5, so z < -0.5 for 60Β° cone + + # Check if asset velocities are small + current_step_stable = torch.ones(env.num_envs, device=env.device, dtype=torch.bool) + for asset in self.assets_to_check: + if isinstance(asset, Articulation): + current_step_stable &= asset.data.joint_vel.abs().sum(dim=1) < 5.0 + elif isinstance(asset, RigidObject): + current_step_stable &= asset.data.body_lin_vel_w.abs().sum(dim=2).sum(dim=1) < 0.05 + current_step_stable &= asset.data.body_ang_vel_w.abs().sum(dim=2).sum(dim=1) < 1.0 + elif isinstance(asset, RigidObjectCollection): + current_step_stable &= asset.data.object_lin_vel_w.abs().sum(dim=2).sum(dim=1) < 0.05 + current_step_stable &= asset.data.object_ang_vel_w.abs().sum(dim=2).sum(dim=1) < 1.0 + + self.stability_counter = torch.where( + current_step_stable, + self.stability_counter + 1, # Increment counter if stable + torch.zeros_like(self.stability_counter), # Reset counter if not stable + ) + + stability_reached = self.stability_counter >= self.consecutive_stability_steps + + # Reset initial positions on first check or after env reset + excessive_pose_deviation = torch.zeros(env.num_envs, device=env.device, dtype=torch.bool) + pos_below_threshold = torch.zeros(env.num_envs, device=env.device, dtype=torch.bool) + for asset in self.assets_to_check: + if asset is self.robot_asset: + asset_pos = asset.data.body_link_pos_w[:, self.ee_body_idx].clone() + else: + asset_pos = asset.data.root_pos_w.clone() + + # Skip if position or quaternion is NaN + pos_is_nan = torch.isnan(asset.data.root_pos_w).any(dim=1) + quat_is_nan = torch.isnan(asset.data.root_quat_w).any(dim=1) + skip_check = pos_is_nan | quat_is_nan + + # Asset has excessive pose deviation if position exceeds thresholds + pos_deviation = (asset_pos - asset.initial_pos).norm(dim=1) + valid_pos_deviation = torch.where(~skip_check, pos_deviation, torch.zeros_like(pos_deviation)) + if asset is self.robot_asset: + excessive_pose_deviation |= valid_pos_deviation > self.max_robot_pos_deviation + else: + excessive_pose_deviation |= valid_pos_deviation > self.max_object_pos_deviation + + # Asset is above ground if position is greater than z threshold + pos_below_threshold |= asset_pos[:, 2] < self.pos_z_threshold + + # Check for collisions between gripper and object + all_env_ids = torch.arange(env.num_envs, device=env.device) + collision_free = torch.all( + torch.stack([collision_analyzer(env, all_env_ids) for collision_analyzer in self.collision_analyzers]), + dim=0, + ) + + reset_success = ( + (~abnormal_gripper_state) + & gripper_orientation_within_range + & stability_reached + & (~excessive_pose_deviation) + & (~pos_below_threshold) + & collision_free + & time_out + ) + + return reset_success + + +class check_obb_no_overlap_termination(ManagerTermBase): + """Termination condition that checks if OBBs of two objects no longer overlap.""" + + def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedEnv): + super().__init__(cfg, env) + + self._env = env + + self.insertive_object_cfg = cfg.params.get("insertive_object_cfg") + self.insertive_object = env.scene[self.insertive_object_cfg.name] + + insertive_metadata = utils.read_metadata_from_usd_directory(self.insertive_object.cfg.spawn.usd_path) + + self.insertive_target_mesh_path = insertive_metadata.get("target_mesh_path") + self.enable_visualization = cfg.params.get("enable_visualization", False) + + # Initialize OBB computation cache and compute OBBs once + self._bbox_cache = bounds_utils.create_bbox_cache() + self._compute_object_obbs() + + # Initialize placeholders for initial poses (will be set in reset) + self._insertive_initial_pos = None + self._insertive_initial_quat = None + + # Store debug draw interface if visualization is enabled + if self.enable_visualization: + import isaacsim.util.debug_draw._debug_draw as omni_debug_draw + + self._omni_debug_draw = omni_debug_draw + else: + self._omni_debug_draw = None + + def _compute_object_obbs(self): + """Compute OBB for insertive object and convert to body frame.""" + # Get prim path (use env 0 as template) + insertive_base_path = self.insertive_object.cfg.prim_path.replace(".*", "0", 1) + + # Determine object prim path - use specific mesh if provided + if self.insertive_target_mesh_path is not None: + insertive_prim_path = f"{insertive_base_path}/{self.insertive_target_mesh_path}" + else: + insertive_prim_path = insertive_base_path + + # Compute OBB in world frame using Isaac Sim's built-in functions + insertive_centroid_world, insertive_axes_world, insertive_half_extents = bounds_utils.compute_obb( + self._bbox_cache, insertive_prim_path + ) + + # Get current world pose of object (env 0) to convert OBB to body frame + insertive_pos_world = self.insertive_object.data.root_pos_w[0] # (3,) + insertive_quat_world = self.insertive_object.data.root_quat_w[0] # (4,) + + device = self._env.device + + # Convert world frame OBB data to torch tensors + insertive_centroid_world_tensor = torch.tensor(insertive_centroid_world, device=device, dtype=torch.float32) + insertive_axes_world_tensor = torch.tensor(insertive_axes_world, device=device, dtype=torch.float32) + + # Convert centroid from world frame to body frame + insertive_centroid_body = math_utils.quat_apply_inverse( + insertive_quat_world, insertive_centroid_world_tensor - insertive_pos_world + ) + + # Convert axes from world frame to body frame + insertive_rot_matrix_world = math_utils.matrix_from_quat(insertive_quat_world.unsqueeze(0))[0] # (3, 3) + + # Transform axes: R_world_to_body @ world_axes = R_world^T @ world_axes + # Note: Isaac Sim's compute_obb returns axes as column vectors, so we need to transpose + insertive_axes_body = torch.matmul(insertive_rot_matrix_world.T, insertive_axes_world_tensor.T).T + + # Cache OBB data in body frame as torch tensors on device for fast access + self._insertive_obb_centroid = insertive_centroid_body + self._insertive_obb_axes = insertive_axes_body + self._insertive_obb_half_extents = torch.tensor(insertive_half_extents, device=device, dtype=torch.float32) + + def reset(self, env_ids: torch.Tensor | None = None) -> None: + """Store initial pose of insertive object when environments are reset.""" + super().reset(env_ids) + + insertive_pos = self.insertive_object.data.root_pos_w.clone() + insertive_quat = self.insertive_object.data.root_quat_w.clone() + + if self._insertive_initial_pos is None or self._insertive_initial_quat is None or env_ids is None: + # First time initialization or reset all environments + self._insertive_initial_pos = insertive_pos + self._insertive_initial_quat = insertive_quat + else: + # Update only the reset environments + self._insertive_initial_pos[env_ids] = insertive_pos[env_ids] + self._insertive_initial_quat[env_ids] = insertive_quat[env_ids] + + def _compute_obb_corners_batch(self, centroids, axes, half_extents): + """ + Compute the 8 corners of Oriented Bounding Boxes for all environments using Isaac Sim's built-in function. + + Args: + centroids: Centers of OBBs (num_envs, 3) + axes: Orientation axes of OBBs (num_envs, 3, 3) - rows are the axes + half_extents: Half extents of OBB along its axes (3,) + + Returns: + corners: 8 corners of the OBBs (num_envs, 8, 3) + """ + num_envs = centroids.shape[0] + device = centroids.device + + # Convert torch tensors to numpy for Isaac Sim functions + centroids_np = centroids.detach().cpu().numpy() + axes_np = axes.detach().cpu().numpy() + half_extents_np = half_extents.detach().cpu().numpy() + + # Compute corners for each environment using Isaac Sim's function + all_corners = [] + for env_idx in range(num_envs): + # Use Isaac Sim's get_obb_corners function + corners_np = bounds_utils.get_obb_corners( + centroids_np[env_idx], axes_np[env_idx], half_extents_np + ) # (8, 3) + all_corners.append(corners_np) + + # Convert back to torch tensor + corners_tensor = torch.tensor(np.stack(all_corners), device=device, dtype=torch.float32) + return corners_tensor # (num_envs, 8, 3) + + def _visualize_bounding_boxes(self, env: ManagerBasedEnv): + """Visualize oriented bounding boxes for initial and current insertive object positions using wireframe edges.""" + # Clear previous debug lines + draw_interface = self._omni_debug_draw.acquire_debug_draw_interface() + draw_interface.clear_lines() + + # Get current world poses of insertive object for all environments + insertive_pos = self.insertive_object.data.root_pos_w # (num_envs, 3) + insertive_quat = self.insertive_object.data.root_quat_w # (num_envs, 4) + + # Transform current insertive object OBB centroid from body frame to world coordinates for all environments + insertive_obb_centroid_body = self._insertive_obb_centroid + insertive_current_world_centroids = insertive_pos + math_utils.quat_apply( + insertive_quat, insertive_obb_centroid_body.unsqueeze(0).expand(env.num_envs, -1) + ) # (num_envs, 3) + + # Transform current insertive object OBB orientation from body frame to world coordinates for all environments + insertive_current_rot_matrices = math_utils.matrix_from_quat(insertive_quat) # (num_envs, 3, 3) + insertive_obb_axes_body = self._insertive_obb_axes + insertive_current_world_axes = torch.bmm( + insertive_current_rot_matrices, + insertive_obb_axes_body.unsqueeze(0).expand(env.num_envs, -1, -1).transpose(1, 2), + ).transpose( + 1, 2 + ) # (num_envs, 3, 3) + + # Compute OBB corners for current position visualization using Isaac Sim's built-in function + insertive_current_corners = self._compute_obb_corners_batch( + insertive_current_world_centroids, insertive_current_world_axes, self._insertive_obb_half_extents + ) # (num_envs, 8, 3) + + # Transform initial insertive object OBB centroid from body frame to world coordinates for all environments + insertive_initial_world_centroids = self._insertive_initial_pos + math_utils.quat_apply( + self._insertive_initial_quat, insertive_obb_centroid_body.unsqueeze(0).expand(env.num_envs, -1) + ) # (num_envs, 3) + + # Transform initial insertive object OBB orientation from body frame to world coordinates + insertive_initial_rot_matrices = math_utils.matrix_from_quat(self._insertive_initial_quat) # (num_envs, 3, 3) + insertive_initial_world_axes = torch.bmm( + insertive_initial_rot_matrices, + insertive_obb_axes_body.unsqueeze(0).expand(env.num_envs, -1, -1).transpose(1, 2), + ).transpose( + 1, 2 + ) # (num_envs, 3, 3) + + # Compute OBB corners for initial position visualization using Isaac Sim's built-in function + insertive_initial_corners = self._compute_obb_corners_batch( + insertive_initial_world_centroids, insertive_initial_world_axes, self._insertive_obb_half_extents + ) # (num_envs, 8, 3) + + # Draw wireframe boxes for each environment + for env_idx in range(env.num_envs): + # Draw current insertive object bounding box edges (blue) + self._draw_obb_wireframe( + insertive_current_corners[env_idx], # (8, 3) + color=(0.0, 0.5, 1.0, 1.0), # Bright blue + line_width=4.0, + draw_interface=draw_interface, + ) + + # Draw initial insertive object bounding box edges (red) + self._draw_obb_wireframe( + insertive_initial_corners[env_idx], # (8, 3) + color=(1.0, 0.2, 0.0, 1.0), # Bright red + line_width=4.0, + draw_interface=draw_interface, + ) + + def _draw_obb_wireframe( + self, corners: torch.Tensor, color: tuple = (1.0, 1.0, 1.0, 1.0), line_width: float = 2.0, draw_interface=None + ): + """ + Draw wireframe edges of an oriented bounding box. + + Args: + corners: 8 corners of the OBB (8, 3) + color: RGBA color tuple for the lines + line_width: Width of the lines + draw_interface: Debug draw interface (optional, will acquire if not provided) + """ + # Define the edges of a cube by connecting corner indices + # Corners are ordered as: [0-3] bottom face, [4-7] top face + edge_indices = [ + # Bottom face edges + (0, 1), + (1, 2), + (2, 3), + (3, 0), + # Top face edges + (4, 5), + (5, 6), + (6, 7), + (7, 4), + # Vertical edges connecting bottom to top + (0, 4), + (1, 5), + (2, 6), + (3, 7), + # Diagonal X on top face (4-5-6-7) + (4, 6), + (5, 7), + # Diagonal X on bottom face (0-1-2-3) + (0, 2), + (1, 3), + # Diagonal X on front face (0-1-4-5) + (0, 5), + (1, 4), + # Diagonal X on back face (2-3-6-7) + (2, 7), + (3, 6), + # Diagonal X on left face (0-3-4-7) + (0, 7), + (3, 4), + # Diagonal X on right face (1-2-5-6) + (1, 6), + (2, 5), + ] + + # Create line segments for all edges + line_starts = [] + line_ends = [] + + for start_idx, end_idx in edge_indices: + line_starts.append(corners[start_idx].cpu().numpy().tolist()) + line_ends.append(corners[end_idx].cpu().numpy().tolist()) + + # Use provided interface or acquire new one + if draw_interface is None: + draw_interface = self._omni_debug_draw.acquire_debug_draw_interface() + + colors = [list(color)] * len(edge_indices) + line_thicknesses = [line_width] * len(edge_indices) + + # Draw all edges at once + draw_interface.draw_lines(line_starts, line_ends, colors, line_thicknesses) + + def __call__( + self, + env: ManagerBasedEnv, + insertive_object_cfg: SceneEntityCfg, + enable_visualization: bool = False, + ) -> torch.Tensor: + """Check if OBB overlap condition is violated between initial and current insertive object positions.""" + + # Get current world poses of insertive object for all environments + insertive_pos = self.insertive_object.data.root_pos_w # (num_envs, 3) + insertive_quat = self.insertive_object.data.root_quat_w # (num_envs, 4) + + # Transform current insertive object centroid from body frame to world coordinates for all environments + insertive_obb_centroid_body = self._insertive_obb_centroid + insertive_current_world_centroids = insertive_pos + math_utils.quat_apply( + insertive_quat, insertive_obb_centroid_body.unsqueeze(0).expand(env.num_envs, -1) + ) # (num_envs, 3) + + # Transform initial insertive object centroid from body frame to world coordinates for all environments + insertive_initial_world_centroids = self._insertive_initial_pos + math_utils.quat_apply( + self._insertive_initial_quat, insertive_obb_centroid_body.unsqueeze(0).expand(env.num_envs, -1) + ) # (num_envs, 3) + + # Transform OBB axes to world coordinates + insertive_current_rot_matrices = math_utils.matrix_from_quat(insertive_quat) # (num_envs, 3, 3) + insertive_initial_rot_matrices = math_utils.matrix_from_quat(self._insertive_initial_quat) # (num_envs, 3, 3) + + insertive_obb_axes_body = self._insertive_obb_axes + + # Transform axes from body frame to world frame: R @ body_axes for all environments + # Since axes are stored as row vectors, we need to handle the transpose properly + insertive_current_world_axes = torch.bmm( + insertive_current_rot_matrices, + insertive_obb_axes_body.unsqueeze(0).expand(env.num_envs, -1, -1).transpose(1, 2), + ).transpose( + 1, 2 + ) # (num_envs, 3, 3) + + insertive_initial_world_axes = torch.bmm( + insertive_initial_rot_matrices, + insertive_obb_axes_body.unsqueeze(0).expand(env.num_envs, -1, -1).transpose(1, 2), + ).transpose( + 1, 2 + ) # (num_envs, 3, 3) + + # Check OBB overlap between initial and current insertive object positions for all environments + obb_overlap = _check_obb_overlap( + insertive_current_world_centroids, + insertive_current_world_axes, + self._insertive_obb_half_extents, + insertive_initial_world_centroids, + insertive_initial_world_axes, + self._insertive_obb_half_extents, + ) + + # Visualize bounding boxes if enabled + if self.enable_visualization: + self._visualize_bounding_boxes(env) + + return ~obb_overlap diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/utils.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/utils.py new file mode 100644 index 0000000..d5ea294 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/reset_states/mdp/utils.py @@ -0,0 +1,336 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +import hashlib +import io +import logging +import numpy as np +import os +import random +import tempfile +import torch +import trimesh +import yaml +from contextlib import contextmanager, redirect_stderr, redirect_stdout +from functools import lru_cache +from urllib.parse import urlparse + +import isaaclab.utils.math as math_utils +import isaacsim.core.utils.torch as torch_utils +import omni +import warp as wp +from isaaclab.utils.assets import retrieve_file_path +from isaaclab.utils.warp import convert_to_warp_mesh +from pxr import UsdGeom +from pytorch3d.ops import sample_farthest_points, sample_points_from_meshes +from pytorch3d.structures import Meshes + +from .rigid_object_hasher import RigidObjectHasher + +# ---- module-scope caches ---- +_PRIM_SAMPLE_CACHE: dict[tuple[str, int], np.ndarray] = {} # (prim_hash, num_points) -> (N,3) in root frame +_FINAL_SAMPLE_CACHE: dict[str, np.ndarray] = {} # env_hash -> (num_points,3) in root frame + + +def clear_pointcloud_caches(): + _PRIM_SAMPLE_CACHE.clear() + _FINAL_SAMPLE_CACHE.clear() + + +@lru_cache(maxsize=None) +def _load_mesh_tensors(prim): + tm = prim_to_trimesh(prim) + verts = torch.from_numpy(tm.vertices.astype("float32")) + faces = torch.from_numpy(tm.faces.astype("int64")) + return verts, faces + + +def sample_object_point_cloud( + num_envs: int, + num_points: int, + prim_path_pattern: str, + device: str = "cuda", # assume GPU + rigid_object_hasher: RigidObjectHasher | None = None, + seed: int = 42, +) -> torch.Tensor | None: + """Generating point cloud given the path regex expression. This methood samples point cloud on ALL colliders + falls under the prim path pattern. It is robust even if there are different numbers of colliders under the same + regex expression. e.g. envs_0/object has 2 colliders, while envs_1/object has 4 colliders. This method will ensure + each object has exactly num_points pointcloud regardless of number of colliders. If detected 0 collider, this method + will return None, indicating no pointcloud can be sampled. + + To save memory and time, this method utilize RigidObjectHasher to make sure collider that hash to the same key will + only be sampled once. It worths noting there are two kinds of hash: + + collider hash, and root hash. As name suggest, collider hash describes the uniqueness of collider from the view of root, + collider hash is generated at atomic level and can not be representing aggregated. The root hash describes the + uniqueness of aggregate of root, and can be hash that represent aggregate of multiple components that composes root. + + Be mindful that root's transform: translation, quaternion, scale, do no account for root's hash + + Args: + num_envs (int): _description_ + num_points (int): _description_ + prim_path_pattern (str): _description_ + device (str, optional): _description_. Defaults to "cuda". + + Returns: + torch.Tensor | None: _description_ + """ + hasher = ( + rigid_object_hasher + if rigid_object_hasher is not None + else RigidObjectHasher(num_envs, prim_path_pattern, device=device) + ) + + if hasher.num_root == 0: + return None + + replicated_env = torch.all(hasher.root_prim_hashes == hasher.root_prim_hashes[0]) + if replicated_env: + # Pick env 0’s colliders + mask_env0 = hasher.collider_prim_env_ids == 0 + verts_list, faces_list = zip(*[_load_mesh_tensors(p) for p, m in zip(hasher.collider_prims, mask_env0) if m]) + meshes = Meshes(verts=[v.to(device) for v in verts_list], faces=[f.to(device) for f in faces_list]) + rel_tf = hasher.collider_prim_relative_transforms[mask_env0] + else: + # Build all envs's colliders + verts_list, faces_list = zip(*[_load_mesh_tensors(p) for p in hasher.collider_prims]) + meshes = Meshes(verts=[v.to(device) for v in verts_list], faces=[f.to(device) for f in faces_list]) + rel_tf = hasher.collider_prim_relative_transforms + with temporary_seed(seed): + # Uniform‐surface sample then scale to root + samp = sample_points_from_meshes(meshes, num_points * 2) + local, _ = sample_farthest_points(samp, K=num_points) + t_rel, q_rel, s_rel = rel_tf[:, :3].unsqueeze(1), rel_tf[:, 3:7].unsqueeze(1), rel_tf[:, 7:].unsqueeze(1) + # here is apply_forward not apply_inverse, because when mesh loaded, it is unscaled. But inorder to view it from + # root, you need to apply forward transformation of root->child, which is exactly tqs_root_child. + root = math_utils.quat_apply(q_rel.expand(-1, num_points, -1), local * s_rel) + t_rel + + # Merge Colliders + if replicated_env: + buf = root.reshape(1, -1, 3) + merged, _ = sample_farthest_points(buf, K=num_points) + result = merged.view(1, num_points, 3).expand(num_envs, -1, -1) * hasher.root_prim_scales.unsqueeze(1) + else: + # 4) Scatter each collider into a padded per‐root buffer + env_ids = hasher.collider_prim_env_ids.to(device) # (M,) + counts = torch.bincount(env_ids, minlength=hasher.num_root) # (num_root,) + max_c = int(counts.max().item()) + buf = torch.zeros((hasher.num_root, max_c * num_points, 3), device=device, dtype=root.dtype) + # track how many placed in each root + placed = torch.zeros_like(counts) + for i in range(len(hasher.collider_prims)): + r = int(env_ids[i].item()) + start = placed[r].item() * num_points + buf[r, start : start + num_points] = root[i] + placed[r] += 1 + # 5) One batch‐FPS to merge per‐root + merged, _ = sample_farthest_points(buf, K=num_points) + result = merged * hasher.root_prim_scales.unsqueeze(1) + + return result + + +def _triangulate_faces(prim) -> np.ndarray: + mesh = UsdGeom.Mesh(prim) + counts = mesh.GetFaceVertexCountsAttr().Get() + indices = mesh.GetFaceVertexIndicesAttr().Get() + faces = [] + it = iter(indices) + for cnt in counts: + poly = [next(it) for _ in range(cnt)] + for k in range(1, cnt - 1): + faces.append([poly[0], poly[k], poly[k + 1]]) + return np.asarray(faces, dtype=np.int64) + + +def create_primitive_mesh(prim) -> trimesh.Trimesh: + prim_type = prim.GetTypeName() + if prim_type == "Cube": + size = UsdGeom.Cube(prim).GetSizeAttr().Get() + return trimesh.creation.box(extents=(size, size, size)) + elif prim_type == "Sphere": + r = UsdGeom.Sphere(prim).GetRadiusAttr().Get() + return trimesh.creation.icosphere(subdivisions=3, radius=r) + elif prim_type == "Cylinder": + c = UsdGeom.Cylinder(prim) + return trimesh.creation.cylinder(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) + elif prim_type == "Capsule": + c = UsdGeom.Capsule(prim) + return trimesh.creation.capsule(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) + elif prim_type == "Cone": # Cone + c = UsdGeom.Cone(prim) + return trimesh.creation.cone(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) + else: + raise KeyError(f"{prim_type} is not a valid primitive mesh type") + + +def prim_to_trimesh(prim, relative_to_world=False) -> trimesh.Trimesh: + if prim.GetTypeName() == "Mesh": + mesh = UsdGeom.Mesh(prim) + verts = np.asarray(mesh.GetPointsAttr().Get(), dtype=np.float32) + faces = _triangulate_faces(prim) + mesh_tm = trimesh.Trimesh(vertices=verts, faces=faces, process=False) + else: + mesh_tm = create_primitive_mesh(prim) + + if relative_to_world: + tf = np.array(omni.usd.get_world_transform_matrix(prim)).T # shape (4,4) + mesh_tm.apply_transform(tf) + + return mesh_tm + + +def fps(points: torch.Tensor, n_samples: int, memory_threashold=2 * 1024**3) -> torch.Tensor: # 2 GiB + device = points.device + N = points.shape[0] + elem_size = points.element_size() + bytes_needed = N * N * elem_size + if bytes_needed <= memory_threashold: + dist_mat = torch.cdist(points, points) + sampled_idx = torch.zeros(n_samples, dtype=torch.long, device=device) + min_dists = torch.full((N,), float("inf"), device=device) + farthest = torch.randint(0, N, (1,), device=device) + for j in range(n_samples): + sampled_idx[j] = farthest + min_dists = torch.minimum(min_dists, dist_mat[farthest].view(-1)) + farthest = torch.argmax(min_dists) + return sampled_idx + logging.warning(f"FPS fallback to iterative (needed {bytes_needed} > {memory_threashold})") + sampled_idx = torch.zeros(n_samples, dtype=torch.long, device=device) + distances = torch.full((N,), float("inf"), device=device) + farthest = torch.randint(0, N, (1,), device=device) + for j in range(n_samples): + sampled_idx[j] = farthest + dist = torch.norm(points - points[farthest], dim=1) + distances = torch.minimum(distances, dist) + farthest = torch.argmax(distances) + return sampled_idx + + +def prim_to_warp_mesh(prim, device, relative_to_world=False) -> wp.Mesh: + if prim.GetTypeName() == "Mesh": + mesh_prim = UsdGeom.Mesh(prim) + points = np.asarray(mesh_prim.GetPointsAttr().Get(), dtype=np.float32) + indices = np.asarray(mesh_prim.GetFaceVertexIndicesAttr().Get(), dtype=np.int32) + else: + mesh = create_primitive_mesh(prim) + points = mesh.vertices.astype(np.float32) + indices = mesh.faces.astype(np.int32) + + if relative_to_world: + tf = np.array(omni.usd.get_world_transform_matrix(prim)).T # (4,4) + points = (points @ tf[:3, :3].T) + tf[:3, 3] + + wp_mesh = convert_to_warp_mesh(points, indices, device=device) + return wp_mesh + + +@wp.kernel +def get_signed_distance( + queries: wp.array(dtype=wp.vec3), # [n_obstacles * E_bad * n_points, 3] + mesh_handles: wp.array(dtype=wp.uint64), # [n_obstacles * E_bad * max_prims] + prim_counts: wp.array(dtype=wp.int32), # [n_obstacles * E_bad] + coll_rel_pos: wp.array(dtype=wp.vec3), # [n_obstacles * E_bad * max_prims, 3] + coll_rel_quat: wp.array(dtype=wp.quat), # [n_obstacles * E_bad * max_prims, 4] + coll_rel_scale: wp.array(dtype=wp.vec3), # [n_obstacles * E_bad * max_prims, 3] + max_dist: float, + check_dist: bool, + num_envs: int, + num_points: int, + max_prims: int, + signs: wp.array(dtype=float), # [E_bad * n_points] +): + tid = wp.tid() + per_obstacle_stride = num_envs * num_points + obstacle_idx = tid // per_obstacle_stride + rem = tid - obstacle_idx * per_obstacle_stride + env_id = rem // num_points # this env_id is index of arange(0, len(env_id)), its sequence, not selective indexing + q = queries[tid] + # accumulator for the lowest‐sign (start large) + best_signed_dist = max_dist + obstacle_env_base = obstacle_idx * num_envs * max_prims + env_id * max_prims + prim_id = obstacle_idx * num_envs + env_id + + for p in range(prim_counts[prim_id]): + index = obstacle_env_base + p + mid = mesh_handles[index] + if mid != 0: + q1 = q - coll_rel_pos[index] + q2 = wp.quat_rotate_inv(coll_rel_quat[index], q1) + crs = coll_rel_scale[index] + q3 = wp.vec3(q2.x / crs.x, q2.y / crs.y, q2.z / crs.z) + mp = wp.mesh_query_point(mid, q3, max_dist) + if mp.result: + if check_dist: + closest = wp.mesh_eval_position(mid, mp.face, mp.u, mp.v) + local_dist = q3 - closest + unscaled_local_dist = wp.vec3(local_dist.x * crs.x, local_dist.y * crs.y, local_dist.z * crs.z) + delta_root = wp.quat_rotate(coll_rel_quat[index], unscaled_local_dist) + dist = wp.length(delta_root) + signed_dist = dist * mp.sign + else: + signed_dist = mp.sign + if signed_dist < best_signed_dist: + best_signed_dist = signed_dist + signs[tid] = best_signed_dist + + +@contextmanager +def temporary_seed(seed: int, restore_numpy: bool = True, restore_python: bool = True): + # snapshot states + cpu_state = torch.get_rng_state() + cuda_states = torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None + np_state = np.random.get_state() if restore_numpy else None + py_state = random.getstate() if restore_python else None + + try: + sink = io.StringIO() + with redirect_stdout(sink), redirect_stderr(sink): + torch_utils.set_seed(seed) + yield + finally: + # restore everything + torch.set_rng_state(cpu_state) + if cuda_states is not None: + torch.cuda.set_rng_state_all(cuda_states) + if np_state is not None: + np.random.set_state(np_state) + if py_state is not None: + random.setstate(py_state) + + +def read_metadata_from_usd_directory(usd_path: str) -> dict: + """Read metadata from metadata.yaml in the same directory as the USD file.""" + # Get the directory containing the USD file + usd_dir = os.path.dirname(usd_path) + + # Look for metadata.yaml in the same directory + metadata_path = os.path.join(usd_dir, "metadata.yaml") + rank = int(os.getenv("RANK", "0")) + download_dir = os.path.join(tempfile.gettempdir(), f"rank_{rank}") + with open(retrieve_file_path(metadata_path, download_dir=download_dir)) as f: + metadata_file = yaml.safe_load(f) + + return metadata_file + + +def compute_assembly_hash(*usd_paths: str) -> str: + """Compute a hash for an assembly based on the USD file paths. + + Args: + *usd_paths: Variable number of USD file paths + + Returns: + A hash string that uniquely identifies the combination of objects + """ + # Extract path suffixes and sort to ensure consistent hash regardless of input order + sorted_paths = sorted(urlparse(path).path for path in usd_paths) + combined = "|".join(sorted_paths) + + full_hash = hashlib.md5(combined.encode()).hexdigest() + return full_hash diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/__init__.py index fcdf886..1fedbe3 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/__init__.py index 13cbbf8..47fdcc9 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/agents/__init__.py deleted file mode 100644 index ac860d6..0000000 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/agents/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/track_goal_leap.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/track_goal_leap.py deleted file mode 100644 index 17e2b9d..0000000 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/leap/track_goal_leap.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. -# All Rights Reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -import uwlab_assets.robots.leap as leap - -from isaaclab.utils import configclass - -from ... import track_goal_env - -episode_length = 50.0 - - -@configclass -class TrackGoalLeap(track_goal_env.TrackGoalEnv): - def __post_init__(self): - super().__post_init__() - self.scene.robot = leap.IMPLICIT_LEAP6D.replace(prim_path="{ENV_REGEX_NS}/Robot") - self.commands.ee_pose.body_name = "palm_lower" - self.rewards.end_effector_position_tracking.params["asset_cfg"].body_names = "palm_lower" - self.rewards.end_effector_position_tracking_fine_grained.params["asset_cfg"].body_names = "palm_lower" - self.rewards.end_effector_orientation_tracking.params["asset_cfg"].body_names = "palm_lower" - self.rewards.end_effector_orientation_tracking_fine_grained.params["asset_cfg"].body_names = "palm_lower" - - -@configclass -class TrackGoalLeapJointPosition(TrackGoalLeap): - actions = leap.LeapJointPositionAction() # type: ignore - - -@configclass -class TrackGoalLeapMcIkAbs(TrackGoalLeap): - actions = leap.LeapMcIkAbsoluteAction() # type: ignore - - -@configclass -class TrackGoalLeapMcIkDel(TrackGoalLeap): - actions = leap.LeapMcIkDeltaAction() # type: ignore diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/__init__.py index 3a1db74..7210e2e 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/rsl_rl_cfg.py index 1c92263..b7f0737 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/agents/rsl_rl_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/tycho_track_goal.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/tycho_track_goal.py index 5e56739..fb8de1b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/tycho_track_goal.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/tycho/tycho_track_goal.py @@ -1,15 +1,15 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -import uwlab_assets.robots.tycho as tycho -import uwlab_assets.robots.tycho.mdp as tycho_mdp - from isaaclab.managers import SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass +import uwlab_assets.robots.tycho as tycho +import uwlab_assets.robots.tycho.mdp as tycho_mdp + import uwlab_tasks.manager_based.manipulation.track_goal.mdp as mdp from ... import track_goal_env diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/__init__.py index 7fbc864..b6aeadb 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/rsl_rl_cfg.py index f4ac2fa..3e13401 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/agents/rsl_rl_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py index 311a51b..4b83932 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/ur5/track_goal_ur5_env_cfg.py @@ -1,13 +1,13 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -import uwlab_assets.robots.ur5 as ur5 - from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass +import uwlab_assets.robots.ur5 as ur5 + from ... import mdp, track_goal_env diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/__init__.py index 0610339..fbdb968 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/__init__.py index ac860d6..31de0c2 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/rsl_rl_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/rsl_rl_cfg.py index 3e7865f..3d1c353 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/rsl_rl_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/agents/rsl_rl_cfg.py @@ -1,10 +1,9 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.utils import configclass - from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py index dab86be..1336d53 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap.py @@ -1,18 +1,19 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause -import uwlab_assets.robots.leap as leap -import uwlab_assets.robots.leap.mdp as leap_mdp -import uwlab_assets.robots.xarm_leap as xarm_leap -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import ObservationTermCfg as ObsTerm from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg from isaaclab.utils import configclass + +import uwlab_assets.robots.leap as leap +import uwlab_assets.robots.leap.mdp as leap_mdp +import uwlab_assets.robots.xarm_leap as xarm_leap +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + from uwlab.envs.mdp.actions import VisualizableJointTargetPositionCfg import uwlab_tasks.manager_based.manipulation.track_goal.mdp as mdp diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap_deployment.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap_deployment.py index e879098..bb91889 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap_deployment.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/config/xarm_leap/track_goal_xarm_leap_deployment.py @@ -1,13 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause import torch -from uwlab_assets.robots.leap.articulation_drive.dynamixel_driver_cfg import DynamixelDriverCfg -from uwlab_assets.robots.xarm.articulation_drive.xarm_driver_cfg import XarmDriverCfg - from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.envs.mdp.actions.actions_cfg import JointPositionActionCfg from isaaclab.managers import EventTermCfg as EventTerm @@ -16,6 +13,10 @@ from isaaclab.managers import SceneEntityCfg from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass + +from uwlab_assets.robots.leap.articulation_drive.dynamixel_driver_cfg import DynamixelDriverCfg +from uwlab_assets.robots.xarm.articulation_drive.xarm_driver_cfg import XarmDriverCfg + from uwlab.assets import ArticulationCfg from uwlab.assets.articulation import BulletArticulationViewCfg from uwlab.envs import RealRLEnvCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/__init__.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/__init__.py index 0d89a2a..7c72747 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/__init__.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/__init__.py @@ -1,9 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from isaaclab.envs.mdp import * + from uwlab.envs.mdp import * from .command_cfg import HandJointCommandCfg diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py index f8e0155..0024a2b 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command_cfg.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command_cfg.py index b55bf6f..4ab3d4c 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command_cfg.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/command_cfg.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py index 467bfa0..fd210cd 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/mdp/rewards.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -42,6 +42,5 @@ def delta_action_l2( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, ) -> torch.Tensor: - asset: Articulation = env.scene[asset_cfg.name] - delta_action = env.action_manager.action - asset.data.joint_pos + delta_action = env.action_manager.action - env.action_manager.prev_action return torch.sum(torch.square(delta_action), dim=1) diff --git a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py index d4f7f15..d3a43f4 100644 --- a/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py +++ b/source/uwlab_tasks/uwlab_tasks/manager_based/manipulation/track_goal/track_goal_env.py @@ -1,12 +1,10 @@ -# Copyright (c) 2024-2025, The UW Lab Project Developers. +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). # All Rights Reserved. # # SPDX-License-Identifier: BSD-3-Clause from dataclasses import MISSING -from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR - import isaaclab.sim as sim_utils from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.envs import ManagerBasedRLEnvCfg @@ -20,6 +18,8 @@ from isaaclab.scene import InteractiveSceneCfg from isaaclab.utils import configclass +from uwlab_assets import UWLAB_CLOUD_ASSETS_DIR + from . import mdp diff --git a/source/uwlab_tasks/uwlab_tasks/utils/__init__.py b/source/uwlab_tasks/uwlab_tasks/utils/__init__.py new file mode 100644 index 0000000..31de0c2 --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/utils/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/uwlab_tasks/uwlab_tasks/utils/hydra.py b/source/uwlab_tasks/uwlab_tasks/utils/hydra.py new file mode 100644 index 0000000..a0d133c --- /dev/null +++ b/source/uwlab_tasks/uwlab_tasks/utils/hydra.py @@ -0,0 +1,269 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import functools +from collections.abc import Callable, Mapping + +try: + import hydra + from hydra import compose, initialize + from hydra.core.config_store import ConfigStore + from hydra.core.hydra_config import HydraConfig + from omegaconf import DictConfig, OmegaConf +except ImportError: + raise ImportError("Hydra is not installed. Please install it by running 'pip install hydra-core'.") + +from isaaclab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg +from isaaclab.envs.utils.spaces import replace_env_cfg_spaces_with_strings, replace_strings_with_env_cfg_spaces +from isaaclab.utils import replace_slices_with_strings, replace_strings_with_slices +from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry + + +def register_task_to_hydra( + task_name: str, agent_cfg_entry_point: str +) -> tuple[ManagerBasedRLEnvCfg | DirectRLEnvCfg, dict]: + """Register the task configuration to the Hydra configuration store. + + This function resolves the configuration file for the environment and agent based on the task's name. + It then registers the configurations to the Hydra configuration store. + + Args: + task_name: The name of the task. + agent_cfg_entry_point: The entry point key to resolve the agent's configuration file. + + Returns: + A tuple containing the parsed environment and agent configuration objects. + """ + # load the configurations + env_cfg = load_cfg_from_registry(task_name, "env_cfg_entry_point") + agent_cfg = None + if agent_cfg_entry_point: + agent_cfg = load_cfg_from_registry(task_name, agent_cfg_entry_point) + # replace gymnasium spaces with strings because OmegaConf does not support them. + # this must be done before converting the env configs to dictionary to avoid internal reinterpretations + env_cfg = replace_env_cfg_spaces_with_strings(env_cfg) + # convert the configs to dictionary + env_cfg_dict = env_cfg.to_dict() + if isinstance(agent_cfg, dict) or agent_cfg is None: + agent_cfg_dict = agent_cfg + else: + agent_cfg_dict = agent_cfg.to_dict() + cfg_dict = {"env": env_cfg_dict, "agent": agent_cfg_dict} + # replace slices with strings because OmegaConf does not support slices + cfg_dict = replace_slices_with_strings(cfg_dict) + # --- ENV variants β†’ register groups + record defaults + register_hydra_group(cfg_dict) + # store the configuration to Hydra + ConfigStore.instance().store(name=task_name, node=OmegaConf.create(cfg_dict), group=None) + return env_cfg, agent_cfg + + +def hydra_task_config(task_name: str, agent_cfg_entry_point: str) -> Callable: + """Decorator to handle the Hydra configuration for a task. + + This decorator registers the task to Hydra and updates the environment and agent configurations from Hydra parsed + command line arguments. + + Args: + task_name: The name of the task. + agent_cfg_entry_point: The entry point key to resolve the agent's configuration file. + + Returns: + The decorated function with the envrionment's and agent's configurations updated from command line arguments. + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + # register the task to Hydra + env_cfg, agent_cfg = register_task_to_hydra(task_name.split(":")[-1], agent_cfg_entry_point) + + # define the new Hydra main function + @hydra.main(config_path=None, config_name=task_name.split(":")[-1], version_base="1.3") + def hydra_main(hydra_env_cfg: DictConfig, env_cfg=env_cfg, agent_cfg=agent_cfg): + # convert to a native dictionary + hydra_env_cfg = OmegaConf.to_container(hydra_env_cfg, resolve=True) + # replace string with slices because OmegaConf does not support slices + hydra_env_cfg = replace_strings_with_slices(hydra_env_cfg) + # update the group configs with Hydra command line arguments + runtime_choice = HydraConfig.get().runtime.choices + resolve_hydra_group_runtime_override(env_cfg, agent_cfg, hydra_env_cfg, runtime_choice) + # update the configs with the Hydra command line arguments + env_cfg.from_dict(hydra_env_cfg["env"]) + # replace strings that represent gymnasium spaces because OmegaConf does not support them. + # this must be done after converting the env configs from dictionary to avoid internal reinterpretations + env_cfg = replace_strings_with_env_cfg_spaces(env_cfg) + # get agent configs + if isinstance(agent_cfg, dict) or agent_cfg is None: + agent_cfg = hydra_env_cfg["agent"] + else: + agent_cfg.from_dict(hydra_env_cfg["agent"]) + # call the original function + func(env_cfg, agent_cfg, *args, **kwargs) + + # call the new Hydra main function + hydra_main() + + return wrapper + + return decorator + + +def hydra_task_compose(task_name: str, agent_cfg_entry_point: str, hydra_args: list) -> Callable: + """Decorator to compose Hydra configuration programmatically for a task. + + Unlike :func:`hydra_task_config`, this variant does not rely on ``@hydra.main``. + It registers the task in Hydra's ConfigStore and uses ``hydra.compose`` with the + provided overrides to build the configuration tree. Variant-group selections are + resolved, gymnasium spaces and Python slices are converted to supported types, + and the decorated function is invoked with materialized configuration objects. + + The decorated function is called as ``func(env_cfg, agent_cfg, *args, **kwargs)`` + and its return value is propagated to the caller. + + Args: + task_name: Registry name of the task (the last token after ":" is used). + agent_cfg_entry_point: Registry key to load the agent configuration; pass + ``None`` or an empty string to skip loading an agent config. + hydra_args: List of Hydra override strings (e.g., + ``["env.physics.dt=0.01", "agent=ppo"]``) applied during composition. + + Returns: + A decorator that composes the config with the given overrides and calls the + wrapped function, returning its result. + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + # register the task to Hydra + env_cfg, agent_cfg = register_task_to_hydra(task_name.split(":")[-1], agent_cfg_entry_point) + + # Initialize Hydra programmatically + with initialize(config_path=None, version_base="1.3"): + # Compose the configuration + hydra_cfg = compose(config_name=task_name, overrides=hydra_args) + # convert to a native dictionary + hydra_cfg = OmegaConf.to_container(hydra_cfg, resolve=True) + # replace string with slices because OmegaConf does not support slices + hydra_cfg = replace_strings_with_slices(hydra_cfg) + # update the group configs with Hydra command line arguments + runtime_args = {term.split("=")[0]: term.split("=")[1] for term in hydra_args} + resolve_hydra_group_runtime_override(env_cfg, agent_cfg, hydra_cfg, runtime_args) + # update the configs with the Hydra command line arguments + env_cfg.from_dict(hydra_cfg["env"]) + # replace strings that represent gymnasium spaces because OmegaConf does not support them. + # this must be done after converting the env configs from dictionary to avoid internal reinterpretations + env_cfg = replace_strings_with_env_cfg_spaces(env_cfg) + if isinstance(agent_cfg, dict) or agent_cfg is None: + agent_cfg = hydra_cfg["agent"] + else: + agent_cfg.from_dict(hydra_cfg["agent"]) + # call the original function + return func(env_cfg, agent_cfg, *args, **kwargs) + + return wrapper + + return decorator + + +def register_hydra_group(cfg_dict: dict) -> None: + """Register Hydra config groups for variant entries and prime defaults. + + The helper inspects the ``env`` and ``agent`` sections of ``cfg_dict`` for ``variants`` mappings, + registers each group/variant pair with Hydra's :class:`~hydra.core.config_store.ConfigStore`, and + records a ``defaults`` list so Hydra selects the ``default`` variant unless overridden. + + Args: + cfg_dict: Mutable configuration dictionary generated for Hydra consumption. + """ + cs = ConfigStore.instance() + default_groups: list[str] = [] + + for section in ("env", "agent"): + section_dict = cfg_dict.get(section, {}) + if isinstance(section_dict, dict) and "variants" in section_dict: + for root_name, root_dict in section_dict["variants"].items(): + group_path = f"{section}.{root_name}" + default_groups.append(group_path) + # register the default node pointing at cfg_dict[section][root_name] + cs.store(group=group_path, name="default", node=getattr_nested(cfg_dict, group_path)) + # register each variant under that group + for variant_name, variant_node in root_dict.items(): + cs.store(group=group_path, name=variant_name, node=variant_node) + + cfg_dict["defaults"] = ["_self_"] + [{g: "default"} for g in default_groups] + + +def resolve_hydra_group_runtime_override( + env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg, + agent_cfg: dict | object, + hydra_cfg: dict, + choices_runtime: dict = {}, +) -> None: + """Resolve runtime Hydra overrides for registered variant groups. + + Hydra tracks user-selected variants under ``HydraConfig.get().runtime.choices``. Given the original + environment and agent configuration objects plus the Hydra-parsed dictionary, this function replaces + the default variant nodes with the selected ones (excluding explicit ``default``) so downstream code + consumes the correct configuration objects and dictionaries. + + This function also works in contexts without ``hydra.main`` (e.g., tests using ``hydra.compose``): + it falls back to reading choices from ``hydra_cfg['hydra']['runtime']['choices']`` if + ``HydraConfig.get()`` is not initialized. + + Args: + env_cfg: Environment configuration object, typically a dataclass with optional ``variants`` mapping. + agent_cfg: Agent configuration, either a mutable mapping or object exposing ``variants`` entries. + hydra_cfg: Native dictionary that mirrors the Hydra config tree, including the ``hydra`` section. + """ + # Try to read choices from HydraConfig; fall back to hydra_cfg dict if unavailable. + vrnt = "variants" + get_variants = lambda c: getattr(c, vrnt, None) or (c.get(vrnt) if isinstance(c, Mapping) else None) # noqa: E731 + is_group_variant = lambda k, v: k.startswith(pref) and k[cut:] in var and v != "default" # noqa: E731 + for sec, cfg in (("env", env_cfg), ("agent", agent_cfg)): + var = get_variants(cfg) + if not var: + continue + pref, cut = f"{sec}.", len(sec) + 1 + choices = {k[cut:]: v for k, v in choices_runtime.items() if is_group_variant(k, v)} + for key, choice in choices.items(): + node = var[key][choice] + setattr_nested(cfg, key, node) + setattr_nested(hydra_cfg[sec], key, node.to_dict() if hasattr(node, "to_dict") else node) + delattr_nested(cfg, vrnt) + delattr_nested(hydra_cfg, f"{sec}.variants") + + +def setattr_nested(obj: object, attr_path: str, value: object) -> None: + attrs = attr_path.split(".") + for attr in attrs[:-1]: + obj = obj[attr] if isinstance(obj, Mapping) else getattr(obj, attr) + if isinstance(obj, Mapping): + obj[attrs[-1]] = value + else: + setattr(obj, attrs[-1], value) + + +def getattr_nested(obj: object, attr_path: str) -> object: + for attr in attr_path.split("."): + obj = obj[attr] if isinstance(obj, Mapping) else getattr(obj, attr) + return obj + + +def delattr_nested(obj: object, attr_path: str) -> None: + """Delete a nested attribute/key strictly (raises on missing path). + + Uses dict indexing and getattr for traversal, mirroring getattr_nested's strictness. + """ + if "." in attr_path: + parent_path, leaf = attr_path.rsplit(".", 1) + parent = getattr_nested(obj, parent_path) # may raise KeyError/AttributeError + else: + parent, leaf = obj, attr_path + if isinstance(parent, Mapping): + del parent[leaf] + else: + delattr(parent, leaf) diff --git a/tools/conftest.py b/tools/conftest.py new file mode 100644 index 0000000..85a8396 --- /dev/null +++ b/tools/conftest.py @@ -0,0 +1,425 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +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 + +import tools.test_settings as test_settings + + +def pytest_ignore_collect(collection_path, config): + # Skip collection and run each test script individually + return True + + +def capture_test_output_with_timeout(cmd, timeout, env): + """Run a command with timeout and capture all output while streaming in real-time.""" + stdout_data = b"" + stderr_data = b"" + + try: + # Use Popen to capture output in real-time + process = subprocess.Popen( + cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=False + ) + + # Set up file descriptors for non-blocking reads + stdout_fd = process.stdout.fileno() + stderr_fd = process.stderr.fileno() + + # Set non-blocking mode (Unix systems only) + try: + import fcntl + + for fd in [stdout_fd, stderr_fd]: + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + except ImportError: + # fcntl not available on Windows, use a simpler approach + pass + + start_time = time.time() + + while process.poll() is None: + # Check for timeout + if time.time() - start_time > timeout: + process.kill() + try: + remaining_stdout, remaining_stderr = process.communicate(timeout=5) + stdout_data += remaining_stdout + stderr_data += remaining_stderr + except subprocess.TimeoutExpired: + process.terminate() + remaining_stdout, remaining_stderr = process.communicate(timeout=1) + stdout_data += remaining_stdout + stderr_data += remaining_stderr + return -1, stdout_data, stderr_data, True # -1 indicates timeout + + # Check for available output + try: + ready_fds, _, _ = select.select([stdout_fd, stderr_fd], [], [], 0.1) + + for fd in ready_fds: + with contextlib.suppress(OSError): + if fd == stdout_fd: + chunk = process.stdout.read(1024) + if chunk: + stdout_data += chunk + # Print to stdout in real-time + sys.stdout.buffer.write(chunk) + sys.stdout.buffer.flush() + elif fd == stderr_fd: + chunk = process.stderr.read(1024) + if chunk: + stderr_data += chunk + # Print to stderr in real-time + sys.stderr.buffer.write(chunk) + sys.stderr.buffer.flush() + except OSError: + # select failed, fall back to simple polling + time.sleep(0.1) + continue + + # Get any remaining output + remaining_stdout, remaining_stderr = process.communicate() + stdout_data += remaining_stdout + stderr_data += remaining_stderr + + return process.returncode, stdout_data, stderr_data, False + + except Exception as e: + return -1, str(e).encode(), b"", False + + +def create_timeout_test_case(test_file, timeout, stdout_data, stderr_data): + """Create a test case entry for a timeout test with captured logs.""" + test_suite = TestSuite(name=f"timeout_{os.path.splitext(os.path.basename(test_file))[0]}") + test_case = TestCase(name="test_execution", classname=os.path.splitext(os.path.basename(test_file))[0]) + + # Create error message with timeout info and captured logs + error_msg = f"Test timed out after {timeout} seconds" + + # Add captured output to error details + details = f"Timeout after {timeout} seconds\n\n" + + if stdout_data: + details += "=== STDOUT ===\n" + details += stdout_data.decode("utf-8", errors="replace") + "\n" + + if stderr_data: + details += "=== STDERR ===\n" + details += stderr_data.decode("utf-8", errors="replace") + "\n" + + error = Error(message=error_msg) + error.text = details + test_case.result = error + + test_suite.add_testcase(test_case) + return test_suite + + +def run_individual_tests(test_files, workspace_root, isaacsim_ci): + """Run each test file separately, ensuring one finishes before starting the next.""" + failed_tests = [] + test_status = {} + + for test_file in test_files: + print(f"\n\nπŸš€ Running {test_file} independently...\n") + # get file name from path + file_name = os.path.basename(test_file) + 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 + ) + + # Prepare command + cmd = [ + sys.executable, + "-m", + "pytest", + "--no-header", + "-c", + f"{workspace_root}/pytest.ini", + f"--junitxml=tests/test-reports-{str(file_name)}.xml", + "--tb=short", + ] + + if isaacsim_ci: + cmd.append("-m") + cmd.append("isaacsim_ci") + + # Add the test file path last + cmd.append(str(test_file)) + + # Run test with timeout and capture output + returncode, stdout_data, stderr_data, timed_out = capture_test_output_with_timeout(cmd, timeout, env) + + if timed_out: + print(f"Test {test_file} timed out after {timeout} seconds...") + failed_tests.append(test_file) + + # Create a special XML report for timeout tests with captured logs + timeout_suite = create_timeout_test_case(test_file, timeout, stdout_data, stderr_data) + timeout_report = JUnitXml() + timeout_report.add_testsuite(timeout_suite) + + # Write timeout report + report_file = f"tests/test-reports-{str(file_name)}.xml" + timeout_report.write(report_file) + + test_status[test_file] = { + "errors": 1, + "failures": 0, + "skipped": 0, + "tests": 1, + "result": "TIMEOUT", + "time_elapsed": timeout, + } + continue + + if returncode != 0: + failed_tests.append(test_file) + + # check report for any failures + report_file = f"tests/test-reports-{str(file_name)}.xml" + if not os.path.exists(report_file): + print(f"Warning: Test report not found at {report_file}") + failed_tests.append(test_file) + test_status[test_file] = { + "errors": 1, # Assume error since we can't read the report + "failures": 0, + "skipped": 0, + "tests": 0, + "result": "FAILED", + "time_elapsed": 0.0, + } + continue + + try: + report = JUnitXml.fromfile(report_file) + + # Rename test suites to be more descriptive + for suite in report: + if suite.name == "pytest": + # Remove .py extension and use the filename as the test suite name + suite_name = os.path.splitext(file_name)[0] + suite.name = suite_name + + # Write the updated report back + report.write(report_file) + + # Parse the integer values with None handling + errors = int(report.errors) if report.errors is not None else 0 + failures = int(report.failures) if report.failures is not None else 0 + skipped = int(report.skipped) if report.skipped is not None else 0 + tests = int(report.tests) if report.tests is not None else 0 + time_elapsed = float(report.time) if report.time is not None else 0.0 + except Exception as e: + print(f"Error reading test report {report_file}: {e}") + failed_tests.append(test_file) + test_status[test_file] = { + "errors": 1, + "failures": 0, + "skipped": 0, + "tests": 0, + "result": "FAILED", + "time_elapsed": 0.0, + } + continue + + # Check if there were any failures + if errors > 0 or failures > 0: + failed_tests.append(test_file) + + test_status[test_file] = { + "errors": errors, + "failures": failures, + "skipped": skipped, + "tests": tests, + "result": "FAILED" if errors > 0 or failures > 0 else "passed", + "time_elapsed": time_elapsed, + } + + print("~~~~~~~~~~~~ Finished running all tests") + + return failed_tests, test_status + + +def pytest_sessionstart(session): + """Intercept pytest startup to execute tests in the correct order.""" + # Get the workspace root directory (one level up from tools) + workspace_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + source_dirs = [ + os.path.join(workspace_root, "scripts"), + os.path.join(workspace_root, "source"), + ] + + # Get filter pattern from environment variable or command line + filter_pattern = os.environ.get("TEST_FILTER_PATTERN", "") + exclude_pattern = os.environ.get("TEST_EXCLUDE_PATTERN", "") + + isaacsim_ci = os.environ.get("ISAACSIM_CI_SHORT", "false") == "true" + + # Also try to get from pytest config + if hasattr(session.config, "option") and hasattr(session.config.option, "filter_pattern"): + filter_pattern = filter_pattern or getattr(session.config.option, "filter_pattern", "") + if hasattr(session.config, "option") and hasattr(session.config.option, "exclude_pattern"): + exclude_pattern = exclude_pattern or getattr(session.config.option, "exclude_pattern", "") + + print("=" * 50) + print("CONFTEST.PY DEBUG INFO") + print("=" * 50) + print(f"Filter pattern: '{filter_pattern}'") + print(f"Exclude pattern: '{exclude_pattern}'") + print(f"TEST_FILTER_PATTERN env var: '{os.environ.get('TEST_FILTER_PATTERN', 'NOT_SET')}'") + print(f"TEST_EXCLUDE_PATTERN env var: '{os.environ.get('TEST_EXCLUDE_PATTERN', 'NOT_SET')}'") + print("=" * 50) + + # Get all test files in the source directories + test_files = [] + + for source_dir in source_dirs: + if not os.path.exists(source_dir): + print(f"Error: source directory not found at {source_dir}") + pytest.exit("Source directory not found", returncode=1) + + for root, _, files in os.walk(source_dir): + for file in files: + if file.startswith("test_") and file.endswith(".py"): + # Skip if the file is in TESTS_TO_SKIP + if file in test_settings.TESTS_TO_SKIP: + print(f"Skipping {file} as it's in the skip list") + continue + + full_path = os.path.join(root, file) + + # Apply include filter + if filter_pattern and filter_pattern not in full_path: + print(f"Skipping {full_path} (does not match include pattern: {filter_pattern})") + continue + + # Apply exclude filter + if exclude_pattern and exclude_pattern in full_path: + print(f"Skipping {full_path} (matches exclude pattern: {exclude_pattern})") + continue + + test_files.append(full_path) + + if isaacsim_ci: + new_test_files = [] + for test_file in test_files: + with open(test_file) as f: + if "@pytest.mark.isaacsim_ci" in f.read(): + new_test_files.append(test_file) + test_files = new_test_files + + if not test_files: + print("No test files found in source directory") + pytest.exit("No test files found", returncode=1) + + print(f"Found {len(test_files)} test files after filtering:") + for test_file in test_files: + print(f" - {test_file}") + + # Run all tests individually + failed_tests, test_status = run_individual_tests(test_files, workspace_root, isaacsim_ci) + + print("failed tests:", failed_tests) + + # Collect reports + print("~~~~~~~~~ Collecting final report...") + + # create new full report + full_report = JUnitXml() + # read all reports and merge them + for report in os.listdir("tests"): + if report.endswith(".xml"): + print(report) + report_file = JUnitXml.fromfile(f"tests/{report}") + full_report += report_file + print("~~~~~~~~~~~~ Writing final report...") + # write content to full report + result_file = os.environ.get("TEST_RESULT_FILE", "full_report.xml") + full_report_path = f"tests/{result_file}" + print(f"Using result file: {result_file}") + full_report.write(full_report_path) + print("~~~~~~~~~~~~ Report written to", full_report_path) + + # print test status in a nice table + # Calculate the number and percentage of passing tests + num_tests = len(test_status) + num_passing = len([test_path for test_path in test_files if test_status[test_path]["result"] == "passed"]) + num_failing = len([test_path for test_path in test_files if test_status[test_path]["result"] == "FAILED"]) + num_timeout = len([test_path for test_path in test_files if test_status[test_path]["result"] == "TIMEOUT"]) + + if num_tests == 0: + passing_percentage = 100 + else: + passing_percentage = num_passing / num_tests * 100 + + # Print summaries of test results + summary_str = "\n\n" + summary_str += "===================\n" + summary_str += "Test Result Summary\n" + summary_str += "===================\n" + + summary_str += f"Total: {num_tests}\n" + summary_str += f"Passing: {num_passing}\n" + summary_str += f"Failing: {num_failing}\n" + summary_str += f"Timeout: {num_timeout}\n" + summary_str += f"Passing Percentage: {passing_percentage:.2f}%\n" + + # Print time elapsed in hours, minutes, seconds + total_time = sum([test_status[test_path]["time_elapsed"] for test_path in test_files]) + + summary_str += f"Total Time Elapsed: {total_time // 3600}h" + summary_str += f"{total_time // 60 % 60}m" + summary_str += f"{total_time % 60:.2f}s" + + summary_str += "\n\n=======================\n" + summary_str += "Per Test Result Summary\n" + summary_str += "=======================\n" + + # Construct table of results per test + per_test_result_table = PrettyTable(field_names=["Test Path", "Result", "Time (s)", "# Tests"]) + per_test_result_table.align["Test Path"] = "l" + per_test_result_table.align["Time (s)"] = "r" + for test_path in test_files: + num_tests_passed = ( + test_status[test_path]["tests"] + - test_status[test_path]["failures"] + - 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']}", + ]) + + summary_str += per_test_result_table.get_string() + + # Print summary to console and log file + print(summary_str) + + # Exit pytest after custom execution to prevent normal pytest from overwriting our report + pytest.exit("Custom test execution completed", returncode=0 if num_failing == 0 else 1) diff --git a/tools/install_deps.py b/tools/install_deps.py new file mode 100644 index 0000000..08dcd50 --- /dev/null +++ b/tools/install_deps.py @@ -0,0 +1,183 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script is a utility to install dependencies mentioned in an extension.toml file of an extension. + +The script takes in two arguments: + +1. type: The type of dependencies to install. It can be one of the following: ['all', 'apt', 'rosdep']. +2. extensions_dir: The path to the directory beneath which we search for extensions. + +The script will search for all extensions in the extensions_dir and then look for an extension.toml file in each +extension's config directory. If the extension.toml file exists, the script will look for the following keys in the +[isaac_lab_settings] section: + +* **apt_deps**: A list of apt packages to install. +* **ros_ws**: The path to the ROS workspace in the extension. If the path is not absolute, the script assumes that + the path is relative to the extension root and resolves it accordingly. + +If the type is 'all', the script will install both apt and rosdep packages. If the type is 'apt', the script will only +install apt packages. If the type is 'rosdep', the script will only install rosdep packages. + +For more information, please check the `documentation`_. + +.. _documentation: https://uw-lab.github.io/UWLab/source/setup/developer.html#extension-dependency-management +""" + +import argparse +import os +import shutil +import toml +from subprocess import PIPE, STDOUT, Popen + +# 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.") +parser.add_argument("extensions_dir", type=str, help="The path to the directory containing extensions.") +parser.add_argument("--ros_distro", type=str, default="humble", help="The ROS distribution to use for rosdep.") + + +def install_apt_packages(paths: list[str]): + """Installs apt packages listed in the extension.toml file for UW Lab extensions. + + For each path in the input list of paths, the function looks in ``{path}/config/extension.toml`` for + the ``[isaac_lab_settings][apt_deps]`` key. It then attempts to install the packages listed in the + value of the key. The function exits on failure to stop the build process from continuing despite missing + dependencies. + + Args: + paths: A list of paths to the extension's root. + + Raises: + SystemError: If 'apt' is not a known command. This is a system error. + """ + for path in paths: + if shutil.which("apt"): + # Check if the extension.toml file exists + if not os.path.exists(f"{path}/config/extension.toml"): + print( + "[WARN] During the installation of 'apt' dependencies, unable to find a" + f" valid file at: {path}/config/extension.toml." + ) + continue + # Load the extension.toml file and check for apt_deps + with open(f"{path}/config/extension.toml") as fd: + ext_toml = toml.load(fd) + if "isaac_lab_settings" in ext_toml and "apt_deps" in ext_toml["isaac_lab_settings"]: + deps = ext_toml["isaac_lab_settings"]["apt_deps"] + print(f"[INFO] Installing the following apt packages: {deps}") + run_and_print(["apt-get", "update"]) + run_and_print(["apt-get", "install", "-y"] + deps) + else: + print(f"[INFO] No apt packages specified for the extension at: {path}") + else: + raise SystemError("Unable to find 'apt' command. Please ensure that 'apt' is installed on your system.") + + +def install_rosdep_packages(paths: list[str], ros_distro: str = "humble"): + """Installs ROS dependencies listed in the extension.toml file for UW Lab extensions. + + For each path in the input list of paths, the function looks in ``{path}/config/extension.toml`` for + the ``[isaac_lab_settings][ros_ws]`` key. It then attempts to install the ROS dependencies under the workspace + listed in the value of the key. The function exits on failure to stop the build process from continuing despite + missing dependencies. + + If the path to the ROS workspace is not absolute, the function assumes that the path is relative to the extension + root and resolves it accordingly. The function also checks if the ROS workspace exists before proceeding with + the installation of ROS dependencies. If the ROS workspace does not exist, the function raises an error. + + Args: + path: A list of paths to the extension roots. + ros_distro: The ROS distribution to use for rosdep. Default is 'humble'. + + Raises: + FileNotFoundError: If a valid ROS workspace is not found while installing ROS dependencies. + SystemError: If 'rosdep' is not a known command. This is raised if 'rosdep' is not installed on the system. + """ + for path in paths: + if shutil.which("rosdep"): + # Check if the extension.toml file exists + if not os.path.exists(f"{path}/config/extension.toml"): + print( + "[WARN] During the installation of 'rosdep' dependencies, unable to find a" + f" valid file at: {path}/config/extension.toml." + ) + continue + # Load the extension.toml file and check for ros_ws + with open(f"{path}/config/extension.toml") as fd: + ext_toml = toml.load(fd) + if "isaac_lab_settings" in ext_toml and "ros_ws" in ext_toml["isaac_lab_settings"]: + # resolve the path to the ROS workspace + ws_path = ext_toml["isaac_lab_settings"]["ros_ws"] + if not os.path.isabs(ws_path): + ws_path = os.path.join(path, ws_path) + # check if the workspace exists + if not os.path.exists(f"{ws_path}/src"): + raise FileNotFoundError( + "During the installation of 'rosdep' dependencies, unable to find a" + f" valid ROS workspace at: {ws_path}." + ) + # install rosdep if not already installed + if not os.path.exists("/etc/ros/rosdep/sources.list.d/20-default.list"): + 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}", + ]) + else: + print(f"[INFO] No rosdep packages specified for the extension at: {path}") + else: + raise SystemError( + "Unable to find 'rosdep' command. Please ensure that 'rosdep' is installed on your system." + "You can install it by running:\n\t sudo apt-get install python3-rosdep" + ) + + +def run_and_print(args: list[str]): + """Runs a subprocess and prints the output to stdout. + + This function wraps Popen and prints the output to stdout in real-time. + + Args: + args: A list of arguments to pass to Popen. + """ + print(f'Running "{args}"') + with Popen(args, stdout=PIPE, stderr=STDOUT, env=os.environ) as p: + while p.poll() is None: + text = p.stdout.read1().decode("utf-8") + print(text, end="", flush=True) + return_code = p.poll() + if return_code != 0: + raise RuntimeError(f'Subprocess with args: "{args}" failed. The returned error code was: {return_code}') + + +def main(): + # Parse the command line arguments + args = parser.parse_args() + # Get immediate children of args.extensions_dir + extension_paths = [os.path.join(args.extensions_dir, x) for x in next(os.walk(args.extensions_dir))[1]] + + # Install dependencies based on the type + if args.type == "all": + install_apt_packages(extension_paths) + install_rosdep_packages(extension_paths, args.ros_distro) + elif args.type == "apt": + install_apt_packages(extension_paths) + elif args.type == "rosdep": + install_rosdep_packages(extension_paths, args.ros_distro) + else: + raise ValueError(f"'Invalid dependency type: '{args.type}'. Available options: ['all', 'apt', 'rosdep'].") + + +if __name__ == "__main__": + main() diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py new file mode 100644 index 0000000..eeaf88f --- /dev/null +++ b/tools/run_all_tests.py @@ -0,0 +1,400 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""A runner script for all the tests within source directory. + +.. code-block:: bash + + ./uwlab.sh -p tools/run_all_tests.py + + # for dry run + ./uwlab.sh -p tools/run_all_tests.py --discover_only + + # for quiet run + ./uwlab.sh -p tools/run_all_tests.py --quiet + + # for increasing timeout (default is 600 seconds) + ./uwlab.sh -p tools/run_all_tests.py --timeout 1000 + +""" + +import argparse +import logging +import os +import re +import subprocess +import sys +import time +from datetime import datetime +from pathlib import Path +from prettytable import PrettyTable + +# Local imports +from test_settings import DEFAULT_TIMEOUT, UWLAB_PATH, PER_TEST_TIMEOUTS, TESTS_TO_SKIP + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Run all tests under current directory.") + # add arguments + parser.add_argument( + "--skip_tests", + default="", + help="Space separated list of tests to skip in addition to those in tests_to_skip.py.", + type=str, + nargs="*", + ) + + # configure default test directory (source directory) + default_test_dir = os.path.join(UWLAB_PATH, "source") + + parser.add_argument( + "--test_dir", type=str, default=default_test_dir, help="Path to the directory containing the tests." + ) + + # configure default logging path based on time stamp + log_file_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".log" + default_log_path = os.path.join(UWLAB_PATH, "logs", "test_results", log_file_name) + + parser.add_argument( + "--log_path", type=str, default=default_log_path, help="Path to the log file to store the results in." + ) + parser.add_argument("--discover_only", action="store_true", help="Only discover and print tests, don't run them.") + parser.add_argument("--quiet", action="store_true", help="Don't print to console, only log to file.") + parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Timeout for each test in seconds.") + parser.add_argument("--extension", type=str, default=None, help="Run tests only for the given extension.") + # parse arguments + args = parser.parse_args() + return args + + +def test_all( + test_dir: str, + tests_to_skip: list[str], + log_path: str, + timeout: float = DEFAULT_TIMEOUT, + per_test_timeouts: dict[str, float] = {}, + discover_only: bool = False, + quiet: bool = False, + extension: str | None = None, +) -> bool: + """Run all tests under the given directory. + + Args: + test_dir: Path to the directory containing the tests. + tests_to_skip: List of tests to skip. + log_path: Path to the log file to store the results in. + timeout: Timeout for each test in seconds. Defaults to DEFAULT_TIMEOUT. + per_test_timeouts: A dictionary of tests and their timeouts in seconds. Any tests not listed here will use the + timeout specified by `timeout`. Defaults to an empty dictionary. + discover_only: If True, only discover and print the tests without running them. Defaults to False. + quiet: If False, print the output of the tests to the terminal console (in addition to the log file). + Defaults to False. + extension: Run tests only for the given extension. Defaults to None, which means all extensions' + tests will be run. + Returns: + True if all un-skipped tests pass or `discover_only` is True. Otherwise, False. + + Raises: + ValueError: If any test to skip is not found under the given `test_dir`. + + """ + # Create the log directory if it doesn't exist + os.makedirs(os.path.dirname(log_path), exist_ok=True) + + # Add file handler to log to file + logging_handlers = [logging.FileHandler(log_path)] + # We also want to print to console + if not quiet: + logging_handlers.append(logging.StreamHandler()) + # Set up logger + logging.basicConfig(level=logging.INFO, format="%(message)s", handlers=logging_handlers) + + all_test_paths, test_paths, skipped_test_paths, test_timeouts = extract_tests_and_timeouts( + test_dir, extension, tests_to_skip, timeout, per_test_timeouts + ) + + # Print tests to be run + logging.info("\n" + "=" * 60 + "\n") + logging.info(f"The following {len(all_test_paths)} tests were found:") + for i, test_path in enumerate(all_test_paths): + logging.info(f"{i + 1:02d}: {test_path}, timeout: {test_timeouts[test_path]}") + logging.info("\n" + "=" * 60 + "\n") + + logging.info(f"The following {len(skipped_test_paths)} tests are marked to be skipped:") + for i, test_path in enumerate(skipped_test_paths): + logging.info(f"{i + 1:02d}: {test_path}") + logging.info("\n" + "=" * 60 + "\n") + + # Exit if only discovering tests + if discover_only: + return True + + results = {} + + # Run each script and store results + for test_path in test_paths: + results[test_path] = {} + before = time.time() + logging.info("\n" + "-" * 60 + "\n") + logging.info(f"[INFO] Running '{test_path}'\n") + try: + completed_process = subprocess.run( + [sys.executable, test_path], check=True, capture_output=True, timeout=test_timeouts[test_path] + ) + except subprocess.TimeoutExpired as e: + logging.error(f"Timeout occurred: {e}") + result = "TIMEDOUT" + stdout = e.stdout + stderr = e.stderr + except subprocess.CalledProcessError as e: + # When check=True is passed to subprocess.run() above, CalledProcessError is raised if the process returns a + # non-zero exit code. The caveat is returncode is not correctly updated in this case, so we simply + # catch the exception and set this test as FAILED + result = "FAILED" + stdout = e.stdout + stderr = e.stderr + except Exception as e: + logging.error(f"Unexpected exception {e}. Please report this issue on the repository.") + result = "FAILED" + stdout = None + stderr = None + else: + result = "COMPLETED" + stdout = completed_process.stdout + stderr = completed_process.stderr + + after = time.time() + time_elapsed = after - before + + # Decode stdout and stderr + stdout = stdout.decode("utf-8") if stdout is not None else "" + stderr = stderr.decode("utf-8") if stderr is not None else "" + + if result == "COMPLETED": + # Check for success message in the output + success_pattern = r"Ran \d+ tests? in [\d.]+s\s+OK" + if re.search(success_pattern, stdout) or re.search(success_pattern, stderr): + result = "PASSED" + else: + result = "FAILED" + + # Write to log file + logging.info(stdout) + logging.info(stderr) + logging.info(f"[INFO] Time elapsed: {time_elapsed:.2f} s") + logging.info(f"[INFO] Result '{test_path}': {result}") + # Collect results + results[test_path]["time_elapsed"] = time_elapsed + results[test_path]["result"] = result + + # Calculate the number and percentage of passing tests + num_tests = len(all_test_paths) + num_passing = len([test_path for test_path in test_paths if results[test_path]["result"] == "PASSED"]) + num_failing = len([test_path for test_path in test_paths if results[test_path]["result"] == "FAILED"]) + num_timing_out = len([test_path for test_path in test_paths if results[test_path]["result"] == "TIMEDOUT"]) + num_skipped = len(skipped_test_paths) + + if num_tests == 0: + passing_percentage = 100 + else: + passing_percentage = (num_passing + num_skipped) / num_tests * 100 + + # Print summaries of test results + summary_str = "\n\n" + summary_str += "===================\n" + summary_str += "Test Result Summary\n" + summary_str += "===================\n" + + summary_str += f"Total: {num_tests}\n" + summary_str += f"Passing: {num_passing}\n" + summary_str += f"Failing: {num_failing}\n" + summary_str += f"Skipped: {num_skipped}\n" + summary_str += f"Timing Out: {num_timing_out}\n" + + summary_str += f"Passing Percentage: {passing_percentage:.2f}%\n" + + # Print time elapsed in hours, minutes, seconds + total_time = sum([results[test_path]["time_elapsed"] for test_path in test_paths]) + + summary_str += f"Total Time Elapsed: {total_time // 3600}h" + summary_str += f"{total_time // 60 % 60}m" + summary_str += f"{total_time % 60:.2f}s" + + summary_str += "\n\n=======================\n" + summary_str += "Per Test Result Summary\n" + summary_str += "=======================\n" + + # Construct table of results per test + per_test_result_table = PrettyTable(field_names=["Test Path", "Result", "Time (s)"]) + per_test_result_table.align["Test Path"] = "l" + per_test_result_table.align["Time (s)"] = "r" + for test_path in test_paths: + per_test_result_table.add_row( + [test_path, results[test_path]["result"], f"{results[test_path]['time_elapsed']:0.2f}"] + ) + + for test_path in skipped_test_paths: + per_test_result_table.add_row([test_path, "SKIPPED", "N/A"]) + + summary_str += per_test_result_table.get_string() + + # Print summary to console and log file + logging.info(summary_str) + + # Only count failing and timing out tests towards failure + return num_failing + num_timing_out == 0 + + +def extract_tests_and_timeouts( + test_dir: str, + extension: str | None = None, + tests_to_skip: list[str] = [], + timeout: float = DEFAULT_TIMEOUT, + per_test_timeouts: dict[str, float] = {}, +) -> tuple[list[str], list[str], list[str], dict[str, float]]: + """Extract all tests under the given directory or extension and their respective timeouts. + + Args: + test_dir: Path to the directory containing the tests. + extension: Run tests only for the given extension. Defaults to None, which means all extensions' + tests will be run. + tests_to_skip: List of tests to skip. + timeout: Timeout for each test in seconds. Defaults to DEFAULT_TIMEOUT. + per_test_timeouts: A dictionary of tests and their timeouts in seconds. Any tests not listed here will use the + timeout specified by `timeout`. Defaults to an empty dictionary. + + Returns: + A tuple containing the paths of all tests, tests to run, tests to skip, and their respective timeouts. + + Raises: + ValueError: If any test to skip is not found under the given `test_dir`. + """ + + # Discover all tests under current directory + all_test_paths = [str(path) for path in Path(test_dir).resolve().rglob("*test_*.py")] + skipped_test_paths = [] + test_paths = [] + # Check that all tests to skip are actually in the tests + for test_to_skip in tests_to_skip: + for test_path in all_test_paths: + if test_to_skip in test_path: + break + else: + raise ValueError(f"Test to skip '{test_to_skip}' not found in tests.") + + # Filter tests by extension + if extension is not None: + all_tests_in_selected_extension = [] + + for test_path in all_test_paths: + # Extract extension name from test path + extension_name = test_path[test_path.find("extensions") :].split("/")[1] + + # Skip tests that are not in the selected extension + if extension_name != extension: + continue + + all_tests_in_selected_extension.append(test_path) + + all_test_paths = all_tests_in_selected_extension + + # Remove tests to skip from the list of tests to run + if len(tests_to_skip) != 0: + for test_path in all_test_paths: + if any([test_to_skip in test_path for test_to_skip in tests_to_skip]): + skipped_test_paths.append(test_path) + else: + test_paths.append(test_path) + else: + test_paths = all_test_paths + + # Sort test paths so they're always in the same order + all_test_paths.sort() + test_paths.sort() + skipped_test_paths.sort() + + # Initialize all tests to have the same timeout + test_timeouts = {test_path: timeout for test_path in all_test_paths} + + # Overwrite timeouts for specific tests + for test_path_with_timeout, test_timeout in per_test_timeouts.items(): + for test_path in all_test_paths: + if test_path_with_timeout in test_path: + test_timeouts[test_path] = test_timeout + + return all_test_paths, test_paths, skipped_test_paths, test_timeouts + + +def warm_start_app(): + """Warm start the app to compile shaders before running the tests.""" + + print("[INFO] Warm starting the simulation app before running tests.") + before = time.time() + # headless experience + warm_start_output = subprocess.run( + [ + sys.executable, + "-c", + "from uwlab.app import AppLauncher; app_launcher = AppLauncher(headless=True); app_launcher.app.close()", + ], + capture_output=True, + ) + if len(warm_start_output.stderr) > 0: + if "omni::fabric::IStageReaderWriter" not in str(warm_start_output.stderr) and "scaling_governor" not in str( + warm_start_output.stderr + ): + logging.error(f"Error warm starting the app: {str(warm_start_output.stderr)}") + exit(1) + + # headless experience with rendering + warm_start_rendering_output = subprocess.run( + [ + sys.executable, + "-c", + ( + "from uwlab.app import AppLauncher; app_launcher = AppLauncher(headless=True," + " enable_cameras=True); app_launcher.app.close()" + ), + ], + capture_output=True, + ) + if len(warm_start_rendering_output.stderr) > 0: + if "omni::fabric::IStageReaderWriter" not in str( + warm_start_rendering_output.stderr + ) and "scaling_governor" not in str(warm_start_output.stderr): + logging.error(f"Error warm starting the app with rendering: {str(warm_start_rendering_output.stderr)}") + exit(1) + + after = time.time() + time_elapsed = after - before + print(f"[INFO] Warm start completed successfully in {time_elapsed:.2f} s") + + +if __name__ == "__main__": + # parse command line arguments + args = parse_args() + + # warm start the app + warm_start_app() + + # add tests to skip to the list of tests to skip + tests_to_skip = TESTS_TO_SKIP + tests_to_skip += args.skip_tests + + # run all tests + test_success = test_all( + test_dir=args.test_dir, + tests_to_skip=tests_to_skip, + log_path=args.log_path, + timeout=args.timeout, + per_test_timeouts=PER_TEST_TIMEOUTS, + discover_only=args.discover_only, + quiet=args.quiet, + extension=args.extension, + ) + # update exit status based on all tests passing or not + if not test_success: + exit(1) diff --git a/tools/run_train_envs.py b/tools/run_train_envs.py new file mode 100644 index 0000000..52c4b49 --- /dev/null +++ b/tools/run_train_envs.py @@ -0,0 +1,79 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +This scripts run training with different RL libraries over a subset of the environments. + +It calls the script ``scripts/reinforcement_learning/${args.lib_name}/train.py`` with the appropriate arguments. +Each training run has the corresponding "commit tag" appended to the run name, which allows comparing different +training logs of the same environments. + +Example usage: + +.. code-block:: bash + # for rsl-rl + python run_train_envs.py --lib-name rsl_rl + +""" + +import argparse +import subprocess + +from test_settings import UWLAB_PATH, TEST_RL_ENVS + + +def parse_args() -> argparse.Namespace: + """Parse the command line arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--lib-name", + type=str, + default="rsl_rl", + choices=["rsl_rl", "skrl", "rl_games", "sb3"], + help="The name of the library to use for training.", + ) + return parser.parse_args() + + +def main(args: argparse.Namespace): + """The main function.""" + # get the git commit hash + git_commit_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip() + + # add run name based on library + if args.lib_name == "rsl_rl": + extra_args = ["--run_name", git_commit_hash] + else: + # TODO: Modify this for other libraries as well to have commit tag in their saved run logs + extra_args = [] + + # train on each environment + for env_name in TEST_RL_ENVS: + # print a colored output to catch the attention of the user + # this should be a multi-line print statement + print("\033[91m==============================================\033[0m") + print("\033[91m==============================================\033[0m") + print(f"\033[91mTraining on {env_name} with {args.lib_name}...\033[0m") + print("\033[91m==============================================\033[0m") + print("\033[91m==============================================\033[0m") + + # run the training script + subprocess.run( + [ + f"{UWLAB_PATH}/uwlab.sh", + "-p", + f"{UWLAB_PATH}/scripts/reinforcement_learning/{args.lib_name}/train.py", + "--task", + env_name, + "--headless", + ] + + extra_args, + check=False, # do not raise an error if the script fails + ) + + +if __name__ == "__main__": + args_cli = parse_args() + main(args_cli) diff --git a/tools/test_settings.py b/tools/test_settings.py new file mode 100644 index 0000000..6513ed5 --- /dev/null +++ b/tools/test_settings.py @@ -0,0 +1,70 @@ +# Copyright (c) 2024-2025, The UW Lab Project Developers. (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All Rights Reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +This file contains the settings for the tests. +""" + +import os + +UWLAB_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +"""Path to the root directory of the UW Lab repository.""" + +DEFAULT_TIMEOUT = 300 +"""The default timeout for each test in seconds.""" + +PER_TEST_TIMEOUTS = { + "test_articulation.py": 500, + "test_stage_in_memory.py": 500, + "test_environments.py": 2500, # This test runs through all the environments for 100 steps each + "test_environments_with_stage_in_memory.py": ( + 2500 + ), # Like the above, with stage in memory and with and without fabric cloning + "test_environment_determinism.py": 1000, # This test runs through many the environments for 100 steps each + "test_factory_environments.py": 1000, # This test runs through Factory environments for 100 steps each + "test_multi_agent_environments.py": 800, # This test runs through multi-agent environments for 100 steps each + "test_generate_dataset.py": 500, # This test runs annotation for 10 demos and generation until one succeeds + "test_pink_ik.py": 1000, # This test runs through all the pink IK environments through various motions + "test_environments_training.py": ( + 6000 + ), # This test runs through training for several environments and compares thresholds + "test_simulation_render_config.py": 500, + "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, +} +"""A dictionary of tests and their timeouts in seconds. + +Note: Any tests not listed here will use the default timeout. +""" + +TESTS_TO_SKIP = [ + # lab + "test_argparser_launch.py", # app.close issue + "test_build_simulation_context_nonheadless.py", # headless + "test_env_var_launch.py", # app.close issue + "test_kwarg_launch.py", # app.close issue + "test_differential_ik.py", # Failing + # lab_tasks + "test_record_video.py", # Failing + "test_tiled_camera_env.py", # Need to improve the logic +] +"""A list of tests to skip by run_tests.py""" + +TEST_RL_ENVS = [ + # classic control + "Isaac-Ant-v0", + "Isaac-Cartpole-v0", + # manipulation + "Isaac-Lift-Cube-Franka-v0", + "Isaac-Open-Drawer-Franka-v0", + # dexterous manipulation + "Isaac-Repose-Cube-Allegro-v0", + # locomotion + "Isaac-Velocity-Flat-Unitree-Go2-v0", + "Isaac-Velocity-Rough-Anymal-D-v0", + "Isaac-Velocity-Rough-G1-v0", +] +"""A list of RL environments to test training on by run_train_envs.py""" diff --git a/uwlab.sh b/uwlab.sh index 01ba3cd..94160e7 100755 --- a/uwlab.sh +++ b/uwlab.sh @@ -1,81 +1,797 @@ #!/usr/bin/env bash -# Exit immediately if a command fails + +# Copyright (c) 2022-2025, The UW Lab Project Developers (https://github.com/uw-lab/UWLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +#== +# Configurations +#== + +# Exits if error occurs set -e -# Define a function to print help -print_help() { - echo "Usage: $(basename "$0") [option]" - echo -e "\t-h, --help Display this help message." - echo -e "\t-i, --install [LIB] Install the uwlab packages. Optionally specify a LIB." - echo -e "\t-f, --format Format the uwlab code." - echo -e "\t-t, --test Run tests for uwlab." +# Set tab-spaces +tabs 4 + +# get source directory +export UWLAB_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +#== +# Helper functions +#== + +# install system dependencies +install_system_deps() { + # check if cmake is already installed + if command -v cmake &> /dev/null; then + echo "[INFO] cmake is already installed." + else + # check if running as root + if [ "$EUID" -ne 0 ]; then + echo "[INFO] Installing system dependencies..." + sudo apt-get update && sudo apt-get install -y --no-install-recommends \ + cmake \ + build-essential + else + echo "[INFO] Installing system dependencies..." + apt-get update && apt-get install -y --no-install-recommends \ + cmake \ + build-essential + fi + fi +} + +# Returns success (exit code 0 / "true") if the detected Isaac Sim version starts with 4.5, +# otherwise returns non-zero ("false"). Works with both symlinked binary installs and pip installs. +is_isaacsim_version_4_5() { + local version="" + local python_exe + python_exe=$(extract_python_exe) + + # 0) Fast path: read VERSION file from the symlinked _isaac_sim directory (binary install) + # If the repository has _isaac_sim β†’ symlink, the VERSION file is the simplest source of truth. + if [[ -f "${UWLAB_PATH}/_isaac_sim/VERSION" ]]; then + # Read first line of the VERSION file; don't fail the whole script on errors. + version=$(head -n1 "${UWLAB_PATH}/_isaac_sim/VERSION" || true) + fi + + # 1) Package-path probe: import isaacsim and walk up to ../../VERSION (pip or nonstandard layouts) + # If we still don't know the version, ask Python where the isaacsim package lives + if [[ -z "$version" ]]; then + local sim_file="" + # Print isaacsim.__file__; suppress errors so set -e won't abort. + sim_file=$("${python_exe}" -c 'import isaacsim, os; print(isaacsim.__file__)' 2>/dev/null || true) + if [[ -n "$sim_file" ]]; then + local version_path + version_path="$(dirname "$sim_file")/../../VERSION" + # If that VERSION file exists, read it. + [[ -f "$version_path" ]] && version=$(head -n1 "$version_path" || true) + fi + fi + + # 2) Fallback: use package metadata via importlib.metadata.version("isaacsim") + if [[ -z "$version" ]]; then + version=$("${python_exe}" <<'PY' 2>/dev/null || true +from importlib.metadata import version, PackageNotFoundError +try: + print(version("isaacsim")) +except PackageNotFoundError: + pass +PY +) + fi + + # Final decision: return success if version begins with "4.5", 0 if match, 1 otherwise. + [[ "$version" == 4.5* ]] +} + +# check if running in docker +is_docker() { + [ -f /.dockerenv ] || \ + grep -q docker /proc/1/cgroup || \ + [[ $(cat /proc/1/comm) == "containerd-shim" ]] || \ + grep -q docker /proc/mounts || \ + [[ "$(hostname)" == *"."* ]] +} + +# check if running on ARM architecture +is_arm() { + [[ "$(uname -m)" == "aarch64" ]] || [[ "$(uname -m)" == "arm64" ]] +} + +ensure_cuda_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 + local torch_ver tv_ver cuda_ver + if is_arm; then + torch_ver="2.9.0" + tv_ver="0.24.0" + cuda_ver="130" + else + torch_ver="2.7.0" + tv_ver="0.22.0" + cuda_ver="128" + fi + + local index="${base_index}/cu${cuda_ver}" + local want_torch="${torch_ver}+cu${cuda_ver}" + + # check current torch version (may be empty) + local cur="" + 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 + return 0 + fi + + # clean install torch + echo "[INFO] Installing torch==${torch_ver} and torchvision==${tv_ver} (cu${cuda_ver}) from ${index}..." + ${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 +extract_isaacsim_path() { + # Use the sym-link path to Isaac Sim directory + local isaac_path=${UWLAB_PATH}/_isaac_sim + # If above path is not available, try to find the path using python + if [ ! -d "${isaac_path}" ]; then + # Use the python executable to get the path + local python_exe=$(extract_python_exe) + # Retrieve the path importing isaac sim and getting the environment path + if [ $(${python_exe} -m pip list | grep -c 'isaacsim-rl') -gt 0 ]; then + local isaac_path=$(${python_exe} -c "import isaacsim; import os; print(os.environ['ISAAC_PATH'])") + fi + fi + # check if there is a path available + if [ ! -d "${isaac_path}" ]; then + # throw an error if no path is found + echo -e "[ERROR] Unable to find the Isaac Sim directory: '${isaac_path}'" >&2 + echo -e "\tThis could be due to the following reasons:" >&2 + echo -e "\t1. Conda environment is not activated." >&2 + echo -e "\t2. Isaac Sim pip package 'isaacsim-rl' is not installed." >&2 + echo -e "\t3. Isaac Sim directory is not available at the default path: ${UWLAB_PATH}/_isaac_sim" >&2 + # exit the script + exit 1 + fi + # return the result + echo ${isaac_path} +} + +# extract the python from isaacsim +extract_python_exe() { + # check if using conda + if ! [[ -z "${CONDA_PREFIX}" ]]; then + # use conda python + local python_exe=${CONDA_PREFIX}/bin/python + elif ! [[ -z "${VIRTUAL_ENV}" ]]; then + # use uv virtual environment python + local python_exe=${VIRTUAL_ENV}/bin/python + else + # use kit python + local python_exe=${UWLAB_PATH}/_isaac_sim/python.sh + + if [ ! -f "${python_exe}" ]; then + # note: we need to check system python for cases such as docker + # inside docker, if user installed into system python, we need to use that + # otherwise, use the python from the kit + if [ $(python -m pip list | grep -c 'isaacsim-rl') -gt 0 ]; then + local python_exe=$(which python) + fi + fi + fi + # check if there is a python path available + if [ ! -f "${python_exe}" ]; then + echo -e "[ERROR] Unable to find any Python executable at path: '${python_exe}'" >&2 + echo -e "\tThis could be due to the following reasons:" >&2 + echo -e "\t1. Conda or uv environment is not activated." >&2 + echo -e "\t2. Isaac Sim pip package 'isaacsim-rl' is not installed." >&2 + echo -e "\t3. Python executable is not available at the default path: ${UWLAB_PATH}/_isaac_sim/python.sh" >&2 + exit 1 + fi + # return the result + echo ${python_exe} +} + +# extract the simulator exe from isaacsim +extract_isaacsim_exe() { + # obtain the isaac sim path + local isaac_path=$(extract_isaacsim_path) + # isaac sim executable to use + local isaacsim_exe=${isaac_path}/isaac-sim.sh + # check if there is a python path available + if [ ! -f "${isaacsim_exe}" ]; then + # check for installation using Isaac Sim pip + # note: pip installed Isaac Sim can only come from a direct + # python environment, so we can directly use 'python' here + if [ $(python -m pip list | grep -c 'isaacsim-rl') -gt 0 ]; then + # Isaac Sim - Python packages entry point + local isaacsim_exe="isaacsim isaacsim.exp.full" + else + echo "[ERROR] No Isaac Sim executable found at path: ${isaac_path}" >&2 + exit 1 + fi + fi + # return the result + echo ${isaacsim_exe} +} + +# find pip command based on virtualization +extract_pip_command() { + # detect if we're in a uv environment + if [ -n "${VIRTUAL_ENV}" ] && [ -f "${VIRTUAL_ENV}/pyvenv.cfg" ] && grep -q "uv" "${VIRTUAL_ENV}/pyvenv.cfg"; then + pip_command="uv pip install" + else + # retrieve the python executable + python_exe=$(extract_python_exe) + pip_command="${python_exe} -m pip install" + fi + + echo ${pip_command} +} + +extract_pip_uninstall_command() { + # detect if we're in a uv environment + if [ -n "${VIRTUAL_ENV}" ] && [ -f "${VIRTUAL_ENV}/pyvenv.cfg" ] && grep -q "uv" "${VIRTUAL_ENV}/pyvenv.cfg"; then + pip_uninstall_command="uv pip uninstall" + else + # retrieve the python executable + python_exe=$(extract_python_exe) + pip_uninstall_command="${python_exe} -m pip uninstall -y" + fi + + echo ${pip_uninstall_command} +} + +# check if input directory is a python extension and install the module +install_uwlab_extension() { + # retrieve the python executable + python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) + + # if the directory contains setup.py then install the python module + if [ -f "$1/setup.py" ]; then + echo -e "\t module: $1" + $pip_command --editable "$1" + fi +} + +# Resolve Torch-bundled libgomp and prepend to LD_PRELOAD, once per shell session +write_torch_gomp_hooks() { + mkdir -p "${CONDA_PREFIX}/etc/conda/activate.d" "${CONDA_PREFIX}/etc/conda/deactivate.d" + + # activation: resolve Torch's libgomp via this env's Python and prepend to LD_PRELOAD + cat > "${CONDA_PREFIX}/etc/conda/activate.d/torch_gomp.sh" <<'EOS' +# Resolve Torch-bundled libgomp and prepend to LD_PRELOAD (quiet + idempotent) +: "${_IL_PREV_LD_PRELOAD:=${LD_PRELOAD-}}" + +__gomp="$("$CONDA_PREFIX/bin/python" - <<'PY' 2>/dev/null || true +import pathlib +try: + import torch + p = pathlib.Path(torch.__file__).parent / 'lib' / 'libgomp.so.1' + print(p if p.exists() else "", end="") +except Exception: + pass +PY +)" + +if [ -n "$__gomp" ] && [ -r "$__gomp" ]; then + case ":${LD_PRELOAD:-}:" in + *":$__gomp:"*) : ;; # already present + *) export LD_PRELOAD="$__gomp${LD_PRELOAD:+:$LD_PRELOAD}";; + esac +fi +unset __gomp +EOS + + # deactivation: restore original LD_PRELOAD + cat > "${CONDA_PREFIX}/etc/conda/deactivate.d/torch_gomp_unset.sh" <<'EOS' +# restore LD_PRELOAD to pre-activation value +if [ -v _IL_PREV_LD_PRELOAD ]; then + export LD_PRELOAD="$_IL_PREV_LD_PRELOAD" + unset _IL_PREV_LD_PRELOAD +fi +EOS +} + +# Temporarily unset LD_PRELOAD (ARM only) for a block of commands +begin_arm_install_sandbox() { + if is_arm && [[ -n "${LD_PRELOAD:-}" ]]; then + export _IL_SAVED_LD_PRELOAD="$LD_PRELOAD" + unset LD_PRELOAD + echo "[INFO] ARM install sandbox: temporarily unsetting LD_PRELOAD for installation." + fi + # ensure we restore even if a command fails (set -e) + trap 'end_arm_install_sandbox' EXIT +} + +end_arm_install_sandbox() { + if [[ -n "${_IL_SAVED_LD_PRELOAD:-}" ]]; then + export LD_PRELOAD="$_IL_SAVED_LD_PRELOAD" + unset _IL_SAVED_LD_PRELOAD + fi + # remove trap so later exits don’t re-run restore + trap - EXIT +} + +# setup anaconda environment for UW Lab +setup_conda_env() { + # get environment name from input + local env_name=$1 + # check conda is installed + if ! command -v conda &> /dev/null + then + echo "[ERROR] Conda could not be found. Please install conda and try again." + exit 1 + fi + + # check if _isaac_sim symlink exists and isaacsim-rl is not installed via pip + if [ ! -L "${UWLAB_PATH}/_isaac_sim" ] && ! python -m pip list | grep -q 'isaacsim-rl'; then + echo -e "[WARNING] _isaac_sim symlink not found at ${UWLAB_PATH}/_isaac_sim" + echo -e "\tThis warning can be ignored if you plan to install Isaac Sim via pip." + echo -e "\tIf you are using a binary installation of Isaac Sim, please ensure the symlink is created before setting up the conda environment." + fi + + # check if the environment exists + if { conda env list | grep -w ${env_name}; } >/dev/null 2>&1; then + echo -e "[INFO] Conda environment named '${env_name}' already exists." + else + echo -e "[INFO] Creating conda environment named '${env_name}'..." + echo -e "[INFO] Installing dependencies from ${UWLAB_PATH}/environment.yml" + + # patch Python version if needed, but back up first + cp "${UWLAB_PATH}/environment.yml"{,.bak} + if is_isaacsim_version_4_5; then + echo "[INFO] Detected Isaac Sim 4.5 β†’ forcing python=3.10" + sed -i 's/^ - python=3\.11/ - python=3.10/' "${UWLAB_PATH}/environment.yml" + else + echo "[INFO] Isaac Sim >= 5.0 detected, installing python=3.11" + fi + + conda env create -y --file ${UWLAB_PATH}/environment.yml -n ${env_name} + # (optional) restore original environment.yml: + if [[ -f "${UWLAB_PATH}/environment.yml.bak" ]]; then + mv "${UWLAB_PATH}/environment.yml.bak" "${UWLAB_PATH}/environment.yml" + fi + fi + + # cache current paths for later + cache_pythonpath=$PYTHONPATH + cache_ld_library_path=$LD_LIBRARY_PATH + # clear any existing files + rm -f ${CONDA_PREFIX}/etc/conda/activate.d/setenv.sh + rm -f ${CONDA_PREFIX}/etc/conda/deactivate.d/unsetenv.sh + # activate the environment + source $(conda info --base)/etc/profile.d/conda.sh + conda activate ${env_name} + # setup directories to load Isaac Sim variables + mkdir -p ${CONDA_PREFIX}/etc/conda/activate.d + mkdir -p ${CONDA_PREFIX}/etc/conda/deactivate.d + + # add variables to environment during activation + printf '%s\n' '#!/usr/bin/env bash' '' \ + '# for UW Lab' \ + 'export UWLAB_PATH='${UWLAB_PATH}'' \ + 'alias uwlab='${UWLAB_PATH}'/uwlab.sh' \ + '' \ + '# show icon if not running headless' \ + 'export RESOURCE_NAME="IsaacSim"' \ + '' > ${CONDA_PREFIX}/etc/conda/activate.d/setenv.sh + + write_torch_gomp_hooks + # check if we have _isaac_sim directory -> if so that means binaries were installed. + # we need to setup conda variables to load the binaries + local isaacsim_setup_conda_env_script=${UWLAB_PATH}/_isaac_sim/setup_conda_env.sh + + if [ -f "${isaacsim_setup_conda_env_script}" ]; then + # add variables to environment during activation + printf '%s\n' \ + '# for Isaac Sim' \ + 'source '${isaacsim_setup_conda_env_script}'' \ + '' >> ${CONDA_PREFIX}/etc/conda/activate.d/setenv.sh + fi + + # reactivate the environment to load the variables + # needed because deactivate complains about UW Lab alias since it otherwise doesn't exist + conda activate ${env_name} + + # remove variables from environment during deactivation + printf '%s\n' '#!/usr/bin/env bash' '' \ + '# for UW Lab' \ + 'unalias uwlab &>/dev/null' \ + 'unset UWLAB_PATH' \ + '' \ + '# restore paths' \ + 'export PYTHONPATH='${cache_pythonpath}'' \ + 'export LD_LIBRARY_PATH='${cache_ld_library_path}'' \ + '' \ + '# for Isaac Sim' \ + 'unset RESOURCE_NAME' \ + '' > ${CONDA_PREFIX}/etc/conda/deactivate.d/unsetenv.sh + + # check if we have _isaac_sim directory -> if so that means binaries were installed. + if [ -f "${isaacsim_setup_conda_env_script}" ]; then + # add variables to environment during activation + printf '%s\n' \ + '# for Isaac Sim' \ + 'unset CARB_APP_PATH' \ + 'unset EXP_PATH' \ + 'unset ISAAC_PATH' \ + '' >> ${CONDA_PREFIX}/etc/conda/deactivate.d/unsetenv.sh + fi + + # deactivate the environment + conda deactivate + # add information to the user about alias + echo -e "[INFO] Added 'uwlab' alias to conda environment for 'uwlab.sh' script." + echo -e "[INFO] Created conda environment named '${env_name}'.\n" + echo -e "\t\t1. To activate the environment, run: conda activate ${env_name}" + echo -e "\t\t2. To install UW Lab extensions, run: uwlab -i" + echo -e "\t\t3. To perform formatting, run: uwlab -f" + echo -e "\t\t4. To deactivate the environment, run: conda deactivate" + echo -e "\n" +} + +# setup uv environment for UW Lab +setup_uv_env() { + # get environment name from input + local env_name="$1" + local python_path="$2" + + # check uv is installed + if ! command -v uv &>/dev/null; then + echo "[ERROR] uv could not be found. Please install uv and try again." + echo "[ERROR] uv can be installed here:" + echo "[ERROR] https://docs.astral.sh/uv/getting-started/installation/" + exit 1 + fi + + # check if _isaac_sim symlink exists and isaacsim-rl is not installed via pip + if [ ! -L "${UWLAB_PATH}/_isaac_sim" ] && ! python -m pip list | grep -q 'isaacsim-rl'; then + echo -e "[WARNING] _isaac_sim symlink not found at ${UWLAB_PATH}/_isaac_sim" + echo -e "\tThis warning can be ignored if you plan to install Isaac Sim via pip." + echo -e "\tIf you are using a binary installation of Isaac Sim, please ensure the symlink is created before setting up the conda environment." + fi + + # check if the environment exists + local env_path="${UWLAB_PATH}/${env_name}" + if [ ! -d "${env_path}" ]; then + echo -e "[INFO] Creating uv environment named '${env_name}'..." + uv venv --clear --python "${python_path}" "${env_path}" + else + echo "[INFO] uv environment '${env_name}' already exists." + fi + + # define root path for activation hooks + local uwlab_root="${UWLAB_PATH}" + + # cache current paths for later + cache_pythonpath=$PYTHONPATH + cache_ld_library_path=$LD_LIBRARY_PATH + + # ensure activate file exists + touch "${env_path}/bin/activate" + + # add variables to environment during activation + cat >> "${env_path}/bin/activate" <&2 } -# If no arguments are provided, show the help and exit. -if [ $# -eq 0 ]; then + +#== +# Main +#== + +# check argument provided +if [ -z "$*" ]; then + echo "[Error] No arguments provided." >&2; print_help exit 0 fi -# Base path to the packages (adjust as needed) -BASE_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )/source" - -# Process the command line arguments +# pass the arguments while [[ $# -gt 0 ]]; do - key="$1" - case $key in - -h|--help) - print_help - exit 0 - ;; + # read the key + case "$1" in -i|--install) - echo "[INFO] Installing uwlab packages..." - echo "Installing uwlab..." - pip install -e "${BASE_PATH}/uwlab" - echo "Installing uwlab_assets..." - pip install -e "${BASE_PATH}/uwlab_assets" - echo "Installing uwlab_tasks..." - pip install -e "${BASE_PATH}/uwlab_tasks" - pip install -e "${BASE_PATH}/uwlab_rl" - echo "[INFO] All packages have been installed in editable mode." + # install system dependencies first + install_system_deps + # install the python packages in UWLab/source directory + echo "[INFO] Installing extensions inside the UW Lab repository..." + python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) + pip_uninstall_command=$(extract_pip_uninstall_command) + + # if on ARM arch, temporarily clear LD_PRELOAD + # LD_PRELOAD is restored below, after installation + begin_arm_install_sandbox + + # --- ensure fork of rsl-rl-lib is used --- + echo "[INFO] Forcing reinstall of rsl-rl-lib from zoctipus/rsl_rl.git@master..." + ${pip_uninstall_command} rsl-rl-lib || true + ${pip_command} --no-cache-dir --force-reinstall --no-deps \ + "rsl-rl-lib @ git+https://github.com/zoctipus/rsl_rl.git@master" + echo "[INFO] Verified: rsl-rl-lib reinstalled from UWLab fork." + + # install pytorch (version based on arch) + ensure_cuda_torch + # recursively look into directories and install them + # this does not check dependencies between extensions + export -f extract_python_exe + export -f extract_pip_command + export -f extract_pip_uninstall_command + export -f install_uwlab_extension + # --- NEW: install upstream isaaclab (GitHub main, editable) --- + echo "[INFO] Installing upstream IsaacLab packages from GitHub (main) in editable mode into ${UWLAB_PATH}/_isaaclab ..." + repo_root="${UWLAB_PATH}/_isaaclab/IsaacLab" + mkdir -p "${UWLAB_PATH}/_isaaclab" + if [ ! -d "${repo_root}/.git" ]; then + echo "[INFO] Cloning IsaacLab repository (branch: main) into ${repo_root} ..." + git clone --depth 1 --branch main https://github.com/isaac-sim/IsaacLab.git "${repo_root}" + else + echo "[INFO] Found existing IsaacLab clone at ${repo_root}; using it." + fi + ${pip_command} -e "${repo_root}/source/isaaclab" --extra-index-url https://pypi.nvidia.com + ${pip_command} -e "${repo_root}/source/isaaclab_assets" --extra-index-url https://pypi.nvidia.com + ${pip_command} -e "${repo_root}/source/isaaclab_tasks" --extra-index-url https://pypi.nvidia.com + ${pip_command} -e "${repo_root}/source/isaaclab_rl[all]" --extra-index-url https://pypi.nvidia.com + echo "[INFO] Upstream IsaacLab packages installed (editable) from local clone at ${repo_root}." + # source directory + find -L "${UWLAB_PATH}/source" -mindepth 1 -maxdepth 1 -type d -exec bash -c 'install_uwlab_extension "{}"' \; + # install the python packages for supported reinforcement learning frameworks + echo "[INFO] Installing extra requirements such as learning frameworks..." + # check if specified which rl-framework to install + if [ -z "$2" ]; then + echo "[INFO] Installing all rl-frameworks..." + framework_name="all" + elif [ "$2" = "none" ]; then + echo "[INFO] No rl-framework will be installed." + framework_name="none" + shift # past argument + else + echo "[INFO] Installing rl-framework: $2" + framework_name=$2 + shift # past argument + fi + # install the learning frameworks specified + ${pip_command} -e "${UWLAB_PATH}/source/uwlab_rl[${framework_name}]" + + # 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 + + # restore LD_PRELOAD if we cleared it + end_arm_install_sandbox + + # check if we are inside a docker container or are building a docker image + # in that case don't setup VSCode since it asks for EULA agreement which triggers user interaction + if is_docker; then + echo "[INFO] Running inside a docker container. Skipping VSCode settings setup." + echo "[INFO] To setup VSCode settings, run 'uwlab -v'." + else + # update the vscode settings + update_vscode_settings + fi + + # unset local variables + unset extract_python_exe + unset extract_pip_command + unset extract_pip_uninstall_command + unset install_uwlab_extension + shift # past argument + ;; + -c|--conda) + # use default name if not provided + if [ -z "$2" ]; then + echo "[INFO] Using default conda environment name: env_uwlab" + conda_env_name="env_uwlab" + else + echo "[INFO] Using conda environment name: $2" + conda_env_name=$2 + shift # past argument + fi + # setup the conda environment for UW Lab + setup_conda_env ${conda_env_name} + shift # past argument + ;; + -u|--uv) + # use default name if not provided + if [ -z "$2" ]; then + echo "[INFO] Using default uv environment name: env_uwlab" + uv_env_name="env_uwlab" + else + echo "[INFO] Using uv environment name: $2" + uv_env_name=$2 + shift # past argument + fi + # setup the uv environment for UW Lab + setup_uv_env ${uv_env_name} + shift # past argument ;; -f|--format) - echo "[INFO] Formatting uwlab code..." - # Reset the PYTHONPATH if using a conda environment to avoid conflicts with pre-commit - if [ -n "${CONDA_DEFAULT_ENV}" ]; then + # reset the python path to avoid conflicts with pre-commit + # this is needed because the pre-commit hooks are installed in a separate virtual environment + # and it uses the system python to run the hooks + if [ -n "${CONDA_DEFAULT_ENV}" ] || [ -n "${VIRTUAL_ENV}" ]; then cache_pythonpath=${PYTHONPATH} export PYTHONPATH="" fi - - # Ensure pre-commit is installed + # run the formatter over the repository + # check if pre-commit is installed if ! command -v pre-commit &>/dev/null; then echo "[INFO] Installing pre-commit..." - pip install pre-commit + pip_command=$(extract_pip_command) + ${pip_command} pre-commit + sudo apt-get install -y pre-commit fi - + # always execute inside the UW Lab directory echo "[INFO] Formatting the repository..." - # Determine the repository root directory (assumes this script is at the repo root) - UWLAB_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" cd ${UWLAB_PATH} pre-commit run --all-files cd - > /dev/null - - # Restore the PYTHONPATH if it was modified - if [ -n "${CONDA_DEFAULT_ENV}" ]; then + # set the python path back to the original value + if [ -n "${CONDA_DEFAULT_ENV}" ] || [ -n "${VIRTUAL_ENV}" ]; then export PYTHONPATH=${cache_pythonpath} fi - shift + + shift # past argument + # exit neatly + break + ;; + -p|--python) + # ensures Kit loads Isaac Sim’s icon instead of a generic icon on aarch64 + if is_arm; then + export RESOURCE_NAME="${RESOURCE_NAME:-IsaacSim}" + fi + # run the python provided by isaacsim + python_exe=$(extract_python_exe) + echo "[INFO] Using python from: ${python_exe}" + shift # past argument + ${python_exe} "$@" + # exit neatly + break + ;; + -s|--sim) + # run the simulator exe provided by isaacsim + isaacsim_exe=$(extract_isaacsim_exe) + echo "[INFO] Running isaac-sim from: ${isaacsim_exe}" + shift # past argument + ${isaacsim_exe} --ext-folder ${UWLAB_PATH}/source $@ + # exit neatly + break + ;; + -n|--new) + # run the template generator script + python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) + shift # past argument + echo "[INFO] Installing template dependencies..." + ${pip_command} -q -r ${UWLAB_PATH}/tools/template/requirements.txt + echo -e "\n[INFO] Running template generator...\n" + ${python_exe} ${UWLAB_PATH}/tools/template/cli.py $@ + # exit neatly break ;; -t|--test) - echo "[INFO] Running tests..." - # TODO: add test here + # run the python provided by isaacsim + python_exe=$(extract_python_exe) + shift # past argument + ${python_exe} -m pytest ${UWLAB_PATH}/tools $@ + # exit neatly + break + ;; + -o|--docker) + # run the docker container helper script + docker_script=${UWLAB_PATH}/docker/container.sh + echo "[INFO] Running docker utility script from: ${docker_script}" + shift # past argument + bash ${docker_script} $@ + # exit neatly + break + ;; + -v|--vscode) + # update the vscode settings + update_vscode_settings + shift # past argument + # exit neatly + break + ;; + -d|--docs) + # build the documentation + echo "[INFO] Building documentation..." + # retrieve the python executable + python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) + # install pip packages + cd ${UWLAB_PATH}/docs + ${pip_command} -r requirements.txt > /dev/null + # build the documentation + ${python_exe} -m sphinx -b html -d _build/doctrees . _build/current + # open the documentation + echo -e "[INFO] To open documentation on default browser, run:" + echo -e "\n\t\txdg-open $(pwd)/_build/current/index.html\n" + # exit neatly + cd - > /dev/null + shift # past argument + # exit neatly + break + ;; + -h|--help) + print_help + exit 0 ;; - *) - echo "[ERROR] Unknown option: $key" + *) # unknown option + echo "[Error] Invalid argument provided: $1" print_help exit 1 ;; esac - shift done