commit 32a241c28f218be149318befefa570b0fcc839cf Author: Upstream Snapshot Date: Fri Aug 28 15:42:17 2026 +0800 Import upstream snapshot c19f713c415a699a79d71cd96aa13c3104a05047 Upstream: https://github.com/michaelgillett/mjlab Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047 Upstream-Branch: main diff --git a/.claude/commands/commit-push-pr.md b/.claude/commands/commit-push-pr.md new file mode 100644 index 0000000..0a624d6 --- /dev/null +++ b/.claude/commands/commit-push-pr.md @@ -0,0 +1,19 @@ +--- +allowed-tools: Bash(git checkout --branch:*), Bash(git add:*), Bash(git status:*), Bash(git push:*), Bash(git commit:*), Bash(gh pr create:*) +description: Commit, push, and open a PR +--- + +## Context + +- Current git status: !`git status` +- Current git diff (staged and unstaged changes): !`git diff HEAD` +- Current branch: !`git branch --show-current` + +## Your task + +Based on the above changes: +1. Create a new branch if on main +2. Create a single commit with an appropriate message +3. Push the branch to origin +4. Create a pull request using `gh pr create` +5. You have the capability to call multiple tools in a single response. You MUST do all of the above in a single message. Do not use any other tools or do anything else. Do not send any other text or messages besides these tool calls. diff --git a/.claude/commands/update-mjwarp.md b/.claude/commands/update-mjwarp.md new file mode 100644 index 0000000..f345864 --- /dev/null +++ b/.claude/commands/update-mjwarp.md @@ -0,0 +1,18 @@ +--- +allowed-tools: Bash(uv lock), Bash(git checkout:*), Bash(git add:*), Bash(git status:*), Bash(git push:*), Bash(git commit:*), Bash(gh pr create:*), Edit, Read +description: Update the mujoco-warp dependency to a given commit +--- + +Update the mujoco-warp dependency to commit $ARGUMENTS. + +Steps: +1. Read `pyproject.toml` and find the `mujoco-warp` line under `[tool.uv.sources]`. +2. Use Edit to replace the current `rev = "..."` value with `rev = "$ARGUMENTS"` on that line. +3. Run `uv lock` to regenerate the lockfile. +4. Create and switch to a new branch named `update-mjwarp/` (e.g. `update-mjwarp/e28c6038`). +5. Stage `pyproject.toml` and `uv.lock`, then commit with message: `Update mujoco-warp to `. +6. Push the branch and open a PR with title `Update mujoco-warp to `. + +Important: +- The commit hash is required. If `$ARGUMENTS` is empty, ask the user for a commit hash. +- Do NOT modify anything else in `pyproject.toml`. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..6c51c58 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,33 @@ +{ + "permissions": { + "allow": [ + "Bash(make:*)", + "Bash(uv run:*)", + "Bash(uv lock:*)", + "Bash(uv sync:*)", + "Bash(uv add:*)", + "Bash(git:*)", + "Bash(gh:*)", + "WebSearch", + "Skill(commit-push-pr)", + "Skill(pr-review-toolkit:review-pr)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "uv run ruff format" + } + ] + } + ] + }, + "enabledPlugins": { + "code-simplifier@claude-plugins-official": true, + "pr-review-toolkit@claude-plugins-official": true + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..986f193 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Large runtime directories +.venv/ +logs/ +wandb/ +artifacts/ +benchmark_results/ +dist/ + +# Build/cache +__pycache__/ +*.pyc +.ruff_cache/ +.pytest_cache/ +.uv-cache/ +*.egg-info/ + +# Git/CI +.git/ +.github/ +.gitignore +.pre-commit-config.yaml + +# IDE/local +.vscode/ +.claude/ +notebooks/ + +# Docker +Dockerfile +.dockerignore + +# Docs build artifacts +docs/source/_build/ +docs/source/generated/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ac2bbb2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,156 @@ +name: tests + +on: + push: + branches: [main] + paths-ignore: + - '**.md' + - '**.rst' + - 'docs/**' + - 'Makefile' + - 'LICENSE' + - 'scripts/benchmarks/**' + pull_request: + branches: [main] + paths-ignore: + - '**.md' + - '**.rst' + - 'docs/**' + - 'Makefile' + - 'LICENSE' + - 'scripts/benchmarks/**' + +env: + UV_FROZEN: "1" + +jobs: + lint-format: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true + version: "0.12.1" + - name: Run lint + run: uvx ruff@0.16.1 check --diff + - name: Run format + run: uvx ruff@0.16.1 format --diff + + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + resolution: ["locked"] + # Also test against a fresh resolve of the latest compatible + # dependencies so a new upstream release breaks a PR instead of a + # release. + include: + - python-version: "3.13" + resolution: "unlocked" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + version: "0.12.1" + - name: Restore Warp kernel cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/warp + key: warp-kernels-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python-version }}-${{ matrix.resolution }}-${{ hashFiles('uv.lock', 'mjlab/**/*.py') }} + restore-keys: | + warp-kernels-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python-version }}-${{ matrix.resolution }}- + - name: Test with python ${{ matrix.python-version }} (locked deps) + if: matrix.resolution == 'locked' + run: uv run --extra cpu pytest + - name: Test with python ${{ matrix.python-version }} (latest deps) + if: matrix.resolution == 'unlocked' + env: + UV_FROZEN: "0" + run: uv run --extra cpu --upgrade pytest + + pyright: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + version: "0.12.1" + - name: Test with python ${{ matrix.python-version }} + run: uv run --extra cpu pyright + + ty-check: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + version: "0.12.1" + - name: Type check with python ${{ matrix.python-version }} + run: uv run --extra cpu ty check + + stubs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true + version: "0.12.1" + - name: Sync environment + run: uv sync --extra cpu + - name: Regenerate MuJoCo stubs + run: bash typings/generate_mujoco_stubs.sh + - name: Verify stubs are up to date + run: | + if ! git diff --exit-code typings/mujoco; then + echo "::error::MuJoCo type stubs are out of date. Run 'make stubs' and commit the result." + exit 1 + fi + + # Mirrors the release smoke test: build the artifacts and import mjlab from an + # isolated, freshly resolved install, so packaging and clean-install issues + # surface on the PR instead of at publish time. + smoke-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true + version: "0.12.1" + - name: Install Python 3.13 + run: uv python install 3.13 + - name: Build + run: uv build + # MUJOCO_GL=disable so `import mujoco` skips its GL backend import; the + # runner has no GL libraries and the smoke test does not render. + - name: Smoke test (wheel) + run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py + env: + MUJOCO_GL: disable + - name: Smoke test (source distribution) + run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py + env: + MUJOCO_GL: disable diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..7679e16 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,39 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + if: github.event.pull_request.user.login == 'kevinzakka' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..daeba7a --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,49 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr *)' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..3e99fa0 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,91 @@ +name: Docker + +on: + workflow_dispatch: + + push: + branches: + - "main" + + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +env: + FORCE_COLOR: 1 + REGISTRY: ghcr.io + IMAGE_NAME: mujocolab/mjlab + +permissions: + id-token: write + packages: write + +jobs: + check_paths: + runs-on: ubuntu-22.04 + outputs: + build: ${{ steps.filter.outputs.any }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - id: filter + uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 + with: + list-files: shell + filters: | + any: + - ".github/workflows/docker.yml" + - "Dockerfile" + + build: + needs: check_paths + if: ${{ needs.check_paths.outputs.build == 'true' }} + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Log into registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: | + type=gha + type=registry,ref=ghcr.io/mujocolab/mjlab/mjlab:buildcache + cache-to: | + type=gha,mode=max + type=registry,ref=ghcr.io/mujocolab/mjlab/mjlab:buildcache,mode=max + platforms: linux/amd64 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..692a5c6 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,47 @@ +name: docs + +on: + push: + branches: + - main + tags: + - 'v*' + +permissions: + contents: write + +env: + UV_FROZEN: "1" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.13' + + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.1" + + - name: Build Sphinx Documentation + run: uv run --group docs sphinx-multiversion docs docs/_build + + - name: Add root redirect + run: echo '' > docs/_build/index.html + + - name: Remove Sphinx build artifacts + run: find docs/_build -type d -name .doctrees -exec rm -rf {} + + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/_build/ + keep_files: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..ebcd7f0 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,37 @@ +name: nightly + +# Early warning: upgrade every dependency to its latest version and run the +# test suite, so an upstream release that breaks mjlab shows up here rather than +# in a user's install or at release time. + +on: + schedule: + - cron: "17 11 * * *" # Daily at 11:17 UTC (off the hour to reduce delay). + workflow_dispatch: + +permissions: + contents: read + +jobs: + latest-deps: + name: Test against latest dependencies (py${{ matrix.python-version }}) + # Skip on forks; scheduled runs only make sense on the canonical repo. + if: github.repository == 'mujocolab/mjlab' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.13"] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + python-version: ${{ matrix.python-version }} + version: "0.12.1" + - name: Upgrade all dependencies to latest + run: uv lock --upgrade + - name: Show dependency tree + run: uv tree + - name: Test + run: uv run --extra cpu pytest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2336ed9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: "Publish" + +on: + push: + tags: + - v* + +jobs: + run: + runs-on: ubuntu-latest + environment: + name: pypi + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.1" + - name: Install Python 3.13 + run: uv python install 3.13 + - name: Build + run: uv build + # MUJOCO_GL=disable so `import mujoco` skips its GL backend import. Without + # it, mujoco 3.10 eagerly imports the EGL bindings at import time (mjlab + # defaults MUJOCO_GL=egl on Linux) and crashes on the runner, which has no + # GL libraries. The smoke test does not render, so disabling GL is fine. + - name: Smoke test (wheel) + run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py + env: + MUJOCO_GL: disable + - name: Smoke test (source distribution) + run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py + env: + MUJOCO_GL: disable + - name: Publish + run: uv publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4144e2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +wandb/ +logs/ +onnx/ +videos/ +__pycache__/ +MUJOCO_LOG.TXT +debug.py +.vscode/ +*.ipynb_checkpoints/ +motions/ +*_rerun* +artifacts/ +.venv/ +render_robots.py +benchmark_results/ + +# Documentation outputs. +**/_build/* +**/generated/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f0c2dbf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.14.14 + hooks: + # Run the linter. + - id: ruff-check + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..681311e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..b65e43f --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,60 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: >- + mjlab: A Lightweight Framework for GPU-Accelerated Robot Learning +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Kevin + family-names: Zakka + email: zakka@berkeley.edu + - given-names: Brent + family-names: Yi + email: brentyi@berkeley.edu + - given-names: Qiayuan + family-names: Liao + email: qiayuanl@berkeley.edu + - given-names: Louis + family-names: Le Lay + email: le.lay.louis@gmail.com + - given-names: Koushil + family-names: Sreenath + - given-names: Pieter + family-names: Abbeel +repository-code: 'https://github.com/mujocolab/mjlab' +keywords: + - mujoco + - mujoco-warp + - simulation + - reinforcement-learning + - robotics +license: Apache-2.0 +commit: 0af4087961d6fbe573243951547eb384983aa9c9 +version: 1.6.0 +date-released: '2026-08-08' +preferred-citation: + type: article + title: >- + mjlab: A Lightweight Framework for GPU-Accelerated Robot Learning + authors: + - given-names: Kevin + family-names: Zakka + - given-names: Qiayuan + family-names: Liao + - given-names: Brent + family-names: Yi + - given-names: Louis + family-names: Le Lay + - given-names: Koushil + family-names: Sreenath + - given-names: Pieter + family-names: Abbeel + year: 2026 + url: https://arxiv.org/abs/2601.22074 + identifiers: + - type: arxiv + value: 2601.22074 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ac4b1db --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# Development Workflow + +**Always use `uv run`, not python**. + +```sh + +# 1. Make changes. + +# 2. Type check. +uv run ty check # Fast +uv run pyright # More thorough, but slower + +# 3. Run tests. +uv run pytest tests/ # Single suite +uv run pytest tests/.py # Specific file + +# 4. Format and lint before committing. +uv run ruff format +uv run ruff check --fix +``` + +We've bundled common commands into a Makefile for convenience. + +```sh +make format # Format and lint +make type # Type-check +make check # make format && make type +make test-fast # Run tests excluding slow ones +make test # Run the full test suite +make docs # Build documentation +``` + +Always run `make check` before committing. This runs formatting, linting, +and type checking. Do not commit code that fails type checking. + +Before creating a PR, ensure all checks pass with `make test`. + +When making user-facing changes, add an entry to `docs/source/changelog.rst` +under the "Upcoming version (not yet released)" section using +Added/Changed/Fixed categories. Reference issues with `:issue:\`123\`` +(renders as a link to the GitHub issue). + +# Commits and PRs + +- Put `Fixes #` at the end of the commit message body, not in + the title. +- PR body should be plain, concise prose. No section headers, checklists, + or structured templates. Describe the problem, what the change does, and + any non-obvious tradeoffs. A good PR description reads like a short + paragraph to a colleague, not a form. +- PR and commit messages are rendered on GitHub, so don't hard-wrap them + at 88 columns. Let each sentence flow on one line. + +Some style guidelines to follow: +- Line length limit is 88 columns. This applies to code, comments, and docstrings. +- Avoid local imports unless they are strictly necessary (e.g. circular imports). +- Tests should follow these principles: + - Use functions and fixtures; do not use test classes. + - Favor targeted, efficient tests over exhaustive edge-case coverage. + - Prefer running individual tests rather than the full test suite to improve iteration speed. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4fec317 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing + +Bug fixes and documentation improvements are always welcome. For new features, please open an issue first so we can discuss whether it fits and work out the design, as we're intentional about keeping the scope focused. + +## Workflow + +1. Fork the repository and create a feature branch. +2. Make your changes. +3. Ensure formatting, type checking, and tests pass: `make test-all`. +4. Submit a pull request. + +Type checking (`make type`) is required, PRs that don't pass will be blocked. You can optionally install pre-commit hooks (`pre-commit install`) to catch issues early. + +## Changelog + +Add entries to the "Upcoming version" section in `docs/source/changelog.rst` under the appropriate category (Added / Changed / Fixed), following [Keep a Changelog](https://keepachangelog.com/) conventions. + +## Getting Help + +- **Issues**: https://github.com/mujocolab/mjlab/issues +- **Discussions**: https://github.com/mujocolab/mjlab/discussions + +## License + +By contributing, you agree your contributions will be licensed under Apache 2.0. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1db4e56 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Refer to uv-docker-example: +# https://github.com/astral-sh/uv-docker-example/blob/main/standalone.Dockerfile +# Note that we use uv to launch, so we omit the second half of the example (non-UV final image) + +FROM nvidia/cuda:12.8.0-runtime-ubuntu24.04 +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y \ + git \ + curl \ + libegl-dev \ + && rm -rf /var/lib/apt/lists/* + +ENV UV_COMPILE_BYTECODE=1 +ENV UV_LINK_MODE=copy +ENV UV_PYTHON_PREFERENCE=only-managed + +RUN uv python install 3.13 + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project --no-editable --no-dev + +ADD . /app + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked --no-editable --no-dev + +ENV MUJOCO_GL=egl +EXPOSE 8080 + +CMD ["uv", "run", "python", "tests/smoke_test.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7eb574f --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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 2025, The mjlab Developers + + 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/Makefile b/Makefile new file mode 100644 index 0000000..75e7754 --- /dev/null +++ b/Makefile @@ -0,0 +1,74 @@ +.PHONY: sync +sync: + uv sync --all-packages --extra cu128 --group dev + +.PHONY: sync-cpu +sync-cpu: + uv sync --all-packages --extra cpu --group dev + +.PHONY: format +format: + uv run ruff format + uv run ruff check --fix + +.PHONY: type +type: + uv run ty check + uv run pyright + +.PHONY: stubs +stubs: + bash typings/generate_mujoco_stubs.sh + +.PHONY: check +check: format type + +.PHONY: test +test: + uv run pytest + +.PHONY: test-fast +test-fast: + uv run pytest -m "not slow" + +.PHONY: test-cpu +test-cpu: + FORCE_CPU=1 uv run pytest + +.PHONY: test-cpu-fast +test-cpu-fast: + FORCE_CPU=1 uv run pytest -m "not slow" + +.PHONY: test-all +test-all: check test + +.PHONY: build +build: + uv build + uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py + uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py + @echo "Build and import test successful" + +.PHONY: docs +docs: + uv run --group docs sphinx-build -j auto docs docs/_build + +.PHONY: docs-multiversion +docs-multiversion: + uv run --group docs sphinx-multiversion docs docs/_build + +.PHONY: docs-watch +docs-watch: + uv run --group docs sphinx-autobuild -j auto docs docs/_build + +.PHONY: publish-test +publish-test: build + uv publish --publish-url https://test.pypi.org/legacy/ + +.PHONY: publish +publish: build + uv publish + +.PHONY: docker-build +docker-build: + docker build -t mjlab:latest . diff --git a/README.md b/README.md new file mode 100644 index 0000000..21eefcb --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +![Project banner](https://raw.githubusercontent.com/mujocolab/mjlab/main/docs/source/_static/mjlab-banner.jpg) + +# mjlab + +[![GitHub Actions](https://img.shields.io/github/actions/workflow/status/mujocolab/mjlab/ci.yml?branch=main)](https://github.com/mujocolab/mjlab/actions/workflows/ci.yml?query=branch%3Amain) +[![Documentation](https://github.com/mujocolab/mjlab/actions/workflows/docs.yml/badge.svg)](https://mujocolab.github.io/mjlab/) +[![License](https://img.shields.io/github/license/mujocolab/mjlab)](https://github.com/mujocolab/mjlab/blob/main/LICENSE) +[![MuJoCo Warp](https://img.shields.io/badge/MuJoCo_Warp-3.11.0-blue)](https://github.com/google-deepmind/mujoco_warp/releases/tag/v3.11.0) +[![Nightly Benchmarks](https://img.shields.io/badge/Nightly-Benchmarks-blue)](https://mujocolab.github.io/mjlab/nightly/) +[![PyPI](https://img.shields.io/pypi/v/mjlab)](https://pypi.org/project/mjlab/) +[![PyPI downloads](https://img.shields.io/pypi/dm/mjlab?color=blue)](https://pypistats.org/packages/mjlab) + +mjlab combines [Isaac Lab](https://github.com/isaac-sim/IsaacLab)'s manager-based API with [MuJoCo Warp](https://github.com/google-deepmind/mujoco_warp), a GPU-accelerated version of [MuJoCo](https://github.com/google-deepmind/mujoco). +The framework provides composable building blocks for environment design, +with minimal dependencies and direct access to native MuJoCo data structures. + +## Getting Started + +mjlab requires an NVIDIA GPU for training. macOS is supported for evaluation only. + +**Try it now:** + +Run the demo (no installation needed): + +```bash +uvx --from mjlab --refresh demo +``` + +Or try in [Google Colab](https://colab.research.google.com/github/mujocolab/mjlab/blob/main/notebooks/demo.ipynb) (no local setup required). + +**Install from source:** + +```bash +git clone https://github.com/mujocolab/mjlab.git && cd mjlab +uv run demo +``` + +For alternative installation methods (PyPI, Docker), see the [Installation Guide](https://mujocolab.github.io/mjlab/main/source/installation.html). + +## Training Examples + +### 1. Velocity Tracking + +Train a Unitree G1 humanoid to follow velocity commands on flat terrain: + +```bash +uv run train Mjlab-Velocity-Flat-Unitree-G1 --env.scene.num-envs 4096 +``` + +**Multi-GPU Training:** Scale to multiple GPUs using `--gpu-ids`: + +```bash +uv run train Mjlab-Velocity-Flat-Unitree-G1 \ + --gpu-ids "[0, 1]" \ + --env.scene.num-envs 4096 +``` + +See the [Distributed Training guide](https://mujocolab.github.io/mjlab/main/source/training/distributed_training.html) for details. + +Evaluate a policy while training (fetches latest checkpoint from Weights & Biases): + +```bash +uv run play Mjlab-Velocity-Flat-Unitree-G1 --wandb-run-path your-org/mjlab/run-id +``` + +### 2. Motion Imitation + +Train a humanoid to mimic reference motions. See the [motion imitation guide](https://mujocolab.github.io/mjlab/main/source/training/motion_imitation.html) for preprocessing setup. + +```bash +uv run train Mjlab-Tracking-Flat-Unitree-G1 --registry-name your-org/motions/motion-name --env.scene.num-envs 4096 +uv run play Mjlab-Tracking-Flat-Unitree-G1 --wandb-run-path your-org/mjlab/run-id +``` + +### 3. Sanity-check with Dummy Agents + +Use built-in agents to sanity check your MDP before training: + +```bash +uv run play Mjlab-Your-Task-Id --agent zero # Sends zero actions +uv run play Mjlab-Your-Task-Id --agent random # Sends uniform random actions +``` + +When running motion-tracking tasks, add `--registry-name your-org/motions/motion-name` to the command. + + +## Documentation + +Full documentation is available at **[mujocolab.github.io/mjlab](https://mujocolab.github.io/mjlab/)**. + +## Development + +```bash +make test # Run all tests +make test-fast # Skip slow tests +make format # Format and lint +make docs # Build docs locally +``` + +For development setup: `uvx pre-commit install` + +## Citation + +mjlab is used in published research and open-source robotics projects. See the [Research](https://mujocolab.github.io/mjlab/main/source/research.html) page for publications and projects, or share your own in [Show and Tell](https://github.com/mujocolab/mjlab/discussions/categories/show-and-tell). + +If you use mjlab in your research, please consider citing: + +```bibtex +@misc{zakka2026mjlablightweightframeworkgpuaccelerated, + title={mjlab: A Lightweight Framework for GPU-Accelerated Robot Learning}, + author={Kevin Zakka and Qiayuan Liao and Brent Yi and Louis Le Lay and Koushil Sreenath and Pieter Abbeel}, + year={2026}, + eprint={2601.22074}, + archivePrefix={arXiv}, + primaryClass={cs.RO}, + url={https://arxiv.org/abs/2601.22074}, +} +``` + +## License + +mjlab is licensed under the [Apache License, Version 2.0](LICENSE). + +### Third-Party Code + +Some portions of mjlab are forked from external projects: + +- **`src/mjlab/utils/lab_api/`** — Utilities forked from [NVIDIA Isaac + Lab](https://github.com/isaac-sim/IsaacLab) (BSD-3-Clause license, see file + headers) + +Forked components retain their original licenses. See file headers for details. + +## Acknowledgments + +mjlab wouldn't exist without the excellent work of the Isaac Lab team, whose API +design and abstractions mjlab builds upon. + +Thanks to the MuJoCo Warp team — especially Erik Frey and Taylor Howell — for +answering our questions, giving helpful feedback, and implementing features +based on our requests countless times. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..84a9b93 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,76 @@ +# Releasing + +## Pre-release checklist + +1. Bump `version` in `pyproject.toml`. +2. Update `version` and `date-released` in `CITATION.cff`. +3. Update the "Upcoming version (not yet released)" heading in `docs/source/changelog.rst` to the new version number and date. +4. Commit the version bump, then create an annotated tag: + +```sh +git tag -a vX.Y.Z -m "Release vX.Y.Z" +git push origin vX.Y.Z +``` + +## Build and verify + +Clean previous build artifacts, then build: + +```sh +rm -rf dist/ +make build +``` + +This runs `uv build` to produce a wheel and sdist in `dist/`, then smoke-tests +both artifacts in isolated environments. + +## Test on TestPyPI (optional but recommended) + +Upload to TestPyPI first to catch packaging issues before the real release: + +```sh +UV_PUBLISH_TOKEN= make publish-test +``` + +Then verify the upload works end-to-end. Use `--index-strategy unsafe-best-match` +because TestPyPI won't have all dependencies and uv needs to fall back to real +PyPI for them: + +```sh +uvx --extra-index-url https://test.pypi.org/simple/ \ + --index-strategy unsafe-best-match \ + --from mjlab \ + demo +``` + +Note: TestPyPI requires a separate account and token from real PyPI. +Generate one at https://test.pypi.org/manage/account/token/. + +## Publish to PyPI + +```sh +UV_PUBLISH_TOKEN= make publish +``` + +Generate a token at https://pypi.org/manage/account/token/. + +## Post-release + +Verify the release installs and runs correctly. Use `--refresh` to bypass +the `uvx` cache (which may still hold the TestPyPI version): + +```sh +uvx --refresh --from mjlab demo +``` + +## Releasing from a past tag + +If the tag has already been created and HEAD has moved ahead, check out the +tag before building: + +```sh +git checkout vX.Y.Z +make build +make publish +git checkout main +``` diff --git a/docs/_templates/versioning.html b/docs/_templates/versioning.html new file mode 100644 index 0000000..8c7af90 --- /dev/null +++ b/docs/_templates/versioning.html @@ -0,0 +1,13 @@ +{% if versions %} + +{% endif %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..897323c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,200 @@ +import os +import sys + +import sphinx_book_theme + +sys.path.insert(0, os.path.abspath("../src")) +sys.path.insert(0, os.path.abspath("../src/mjlab")) + + +project = "mjlab" +copyright = "2025, The mjlab Developers" +author = "The mjlab Developers" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "autodocsumm", + "myst_parser", + "sphinx.ext.napoleon", + "sphinxemoji.sphinxemoji", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinxcontrib.bibtex", + "sphinxcontrib.icon", + "sphinx_copybutton", + "sphinx_design", + "sphinx_tabs.tabs", + "sphinx_multiversion", + "sphinx.ext.extlinks", +] + +extlinks = { + "issue": ( + "https://github.com/mujocolab/mjlab/issues/%s", + "#%s", + ), +} + +mathjax3_config = { + "tex": { + "inlineMath": [["\\(", "\\)"]], + "displayMath": [["\\[", "\\]"]], + }, +} + +panels_add_bootstrap_css = False +panels_add_fontawesome_css = True + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +nitpick_ignore = [ + ("py:obj", "slice(None)"), +] + +nitpick_ignore_regex = [ + (r"py:.*", r"pxr.*"), + (r"py:.*", r"trimesh.*"), +] + +# emoji style +sphinxemoji_style = "twemoji" +autodoc_typehints = "signature" +autoclass_content = "class" +autodoc_class_signature = "separated" +autodoc_member_order = "bysource" +autodoc_inherit_docstrings = True +bibtex_bibfiles = ["source/_static/refs.bib"] +autosummary_generate = True +autosummary_generate_overwrite = False +autodoc_default_options = { + "member-order": "bysource", +} +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + +exclude_patterns = [ + "_build", + "_redirect", + "_templates", + "Thumbs.db", + ".DS_Store", + "README.md", + "licenses/*", +] + +autodoc_mock_imports = [ + "matplotlib", + "scipy", + "carb", + "warp", + "pxr", + "h5py", + "hid", + "prettytable", + "tqdm", + "tensordict", + "trimesh", + "toml", + "mjviser", + "mujoco_warp", + "gymnasium", + "rsl_rl", + "viser", + "wandb", + "torchvision", +] + +suppress_warnings = [ + "ref.python", + "docutils", +] + +language = "en" + +html_title = "mjlab Documentation" +html_theme_path = [sphinx_book_theme.get_html_theme_path()] +html_theme = "sphinx_book_theme" +html_favicon = "source/_static/favicon.ico" +html_show_copyright = True +html_show_sphinx = False +html_last_updated_fmt = "" + +html_static_path = ["source/_static"] +html_css_files = ["css/custom.css"] + +html_theme_options = { + "path_to_docs": "docs/", + "collapse_navigation": True, + "repository_url": "https://github.com/mujocolab/mjlab", + "use_repository_button": True, + "use_issues_button": True, + "use_edit_page_button": True, + "show_toc_level": 2, + "use_sidenotes": True, + "logo": { + "text": "mjlab Documentation", + }, + "icon_links": [ + { + "name": "Benchmarks", + "url": "https://mujocolab.github.io/mjlab/nightly/", + "icon": "fa-solid fa-chart-line", + "type": "fontawesome", + }, + ], + "icon_links_label": "Quick Links", +} + +templates_path = [ + "_templates", +] + +smv_remote_whitelist = r"^.*$" +smv_branch_whitelist = os.getenv("SMV_BRANCH_WHITELIST", r"^(main|devel)$") +smv_tag_whitelist = os.getenv("SMV_TAG_WHITELIST", r"^v[1-9]\d*\.\d+\.\d+$") + +html_sidebars = { + "**": [ + "navbar-logo.html", + "search-field.html", + "versioning.html", + "sbt-sidebar-nav.html", + ] +} + + +def skip_member(app, what, name, obj, skip, options): + exclusions = ["from_dict", "to_dict", "replace", "copy", "validate", "__post_init__"] + if name in exclusions: + return True + return None + + +def process_signature(app, what, name, obj, options, signature, return_annotation): + """Suppress the ugly __init__ signature for dataclass Cfg classes.""" + if what == "class" and "exclude-members" in options: + if "__init__" in options["exclude-members"]: + return ("", None) + return None + + +def process_docstring(app, what, name, obj, options, lines): + """Strip auto-generated dataclass docstrings (e.g. 'ClassName(*, ...)').""" + import dataclasses + + if what == "class" and dataclasses.is_dataclass(obj): + if lines and lines[0].startswith(f"{obj.__name__}("): + lines.clear() + + +def setup(app): + app.connect("autodoc-skip-member", skip_member) + app.connect("autodoc-process-signature", process_signature) + app.connect("autodoc-process-docstring", process_docstring) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..e6123f5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,128 @@ +Welcome to mjlab! +================= + +.. figure:: source/_static/mjlab-banner.jpg + :width: 100% + :alt: mjlab + +mjlab is a lightweight, open-source framework for robot learning that +combines GPU-accelerated simulation with composable environments and minimal +setup friction. It adopts the manager-based API introduced by +`Isaac Lab `_, where users compose +modular building blocks for observations, rewards, and events, and pairs it +with `MuJoCo Warp `_ for +GPU-accelerated physics. The result is a framework installable with a single +command, requiring minimal dependencies, and providing direct access to +native `MuJoCo `_ data +structures. + +**Key features:** + +- **Composable environments:** users define observations, rewards, + terminations, and other MDP terms as modular building blocks +- **Minimal dependencies:** single-command install via ``uv``, low startup + latency +- **Direct MuJoCo data structures:** native ``MjModel``/``MjData`` access + with no translation layers +- **PyTorch-native:** observations, rewards, and actions are PyTorch + tensors backed by zero-copy GPU memory sharing + +For more on the design decisions behind mjlab, see :doc:`source/motivation`. + +**Try it now** (no installation needed): + +.. code-block:: bash + + uvx --from mjlab --refresh demo + +Table of Contents +----------------- + +.. toctree:: + :maxdepth: 1 + :caption: User Guide + + source/installation + source/tutorials + source/contributing + +.. toctree:: + :maxdepth: 1 + :caption: Concepts + + source/architecture_overview + source/entity/index + source/actuators + source/sensors/index + source/scene + source/terrain + +.. toctree:: + :maxdepth: 1 + :caption: The Manager Layer + + source/environment_config + source/observations + source/actions + source/rewards + source/terminations + source/commands + source/events + source/randomization + source/curriculum + source/metrics + source/recorders + +.. toctree:: + :maxdepth: 1 + :caption: Training & Debugging + + source/training/rsl_rl + source/viewers + source/training/distributed_training + source/training/cloud + source/debugging/nan_guard + source/debugging/export_scene + +.. toctree:: + :maxdepth: 2 + :caption: API Reference + + source/api/index + +.. toctree:: + :maxdepth: 1 + :caption: Further Reading + + source/motivation + source/migration_isaac_lab + source/faq + source/research + source/changelog + +License & citation +------------------ + +mjlab is licensed under the Apache License, Version 2.0. +Please refer to the `LICENSE file `_ for details. + +If you use mjlab in your research, we would appreciate a citation: + +.. code-block:: bibtex + + @article{Zakka_mjlab_A_Lightweight_2026, + author = {Zakka, Kevin and Liao, Qiayuan and Yi, Brent and Le Lay, Louis and Sreenath, Koushil and Abbeel, Pieter}, + title = {{mjlab: A Lightweight Framework for GPU-Accelerated Robot Learning}}, + url = {https://arxiv.org/abs/2601.22074}, + year = {2026} + } + +Acknowledgments +--------------- + +mjlab would not exist without the excellent work of the Isaac Lab team, whose API design +and abstractions mjlab builds upon. + +Thanks also to the MuJoCo Warp team — especially Erik Frey and Taylor Howell — for +answering our questions, giving helpful feedback, and implementing features based +on our requests countless times. diff --git a/docs/source/_static/architecture_diagram.png b/docs/source/_static/architecture_diagram.png new file mode 100644 index 0000000..b6e8ac8 Binary files /dev/null and b/docs/source/_static/architecture_diagram.png differ diff --git a/docs/source/_static/changelog/mat_texid_dr.gif b/docs/source/_static/changelog/mat_texid_dr.gif new file mode 100644 index 0000000..31c1521 Binary files /dev/null and b/docs/source/_static/changelog/mat_texid_dr.gif differ diff --git a/docs/source/_static/changelog/native_reward.png b/docs/source/_static/changelog/native_reward.png new file mode 100644 index 0000000..0d0c572 Binary files /dev/null and b/docs/source/_static/changelog/native_reward.png differ diff --git a/docs/source/_static/changelog/terrain_visualizer.jpg b/docs/source/_static/changelog/terrain_visualizer.jpg new file mode 100644 index 0000000..03a29f1 Binary files /dev/null and b/docs/source/_static/changelog/terrain_visualizer.jpg differ diff --git a/docs/source/_static/content/cartpole-env.jpg b/docs/source/_static/content/cartpole-env.jpg new file mode 100644 index 0000000..d65b9fb Binary files /dev/null and b/docs/source/_static/content/cartpole-env.jpg differ diff --git a/docs/source/_static/content/cartpole_trained.gif b/docs/source/_static/content/cartpole_trained.gif new file mode 100644 index 0000000..0586ac5 Binary files /dev/null and b/docs/source/_static/content/cartpole_trained.gif differ diff --git a/docs/source/_static/content/g1.png b/docs/source/_static/content/g1.png new file mode 100644 index 0000000..52daa42 Binary files /dev/null and b/docs/source/_static/content/g1.png differ diff --git a/docs/source/_static/content/go1.png b/docs/source/_static/content/go1.png new file mode 100644 index 0000000..b1f5eb1 Binary files /dev/null and b/docs/source/_static/content/go1.png differ diff --git a/docs/source/_static/content/mjlab-banner.jpg b/docs/source/_static/content/mjlab-banner.jpg new file mode 100644 index 0000000..eed1fc7 Binary files /dev/null and b/docs/source/_static/content/mjlab-banner.jpg differ diff --git a/docs/source/_static/content/nan_debug.gif b/docs/source/_static/content/nan_debug.gif new file mode 100644 index 0000000..22814cd Binary files /dev/null and b/docs/source/_static/content/nan_debug.gif differ diff --git a/docs/source/_static/content/rough_terrain.png b/docs/source/_static/content/rough_terrain.png new file mode 100644 index 0000000..8af9eb6 Binary files /dev/null and b/docs/source/_static/content/rough_terrain.png differ diff --git a/docs/source/_static/content/yam.png b/docs/source/_static/content/yam.png new file mode 100644 index 0000000..4cc444d Binary files /dev/null and b/docs/source/_static/content/yam.png differ diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css new file mode 100644 index 0000000..2a1adc8 --- /dev/null +++ b/docs/source/_static/css/custom.css @@ -0,0 +1,172 @@ +/* + * PyData Sphinx Theme — Option A (Indigo/Teal) + * Aesthetic: modern lab — indigo primary, teal accent, neutral grays + */ + +/* LIGHT THEME */ +html[data-theme="light"] { + /* Brand */ + --pst-color-primary: #4F46E5; + /* Indigo-600 */ + --pst-color-secondary: #14B8A6; + /* Teal-500 */ + --pst-color-secondary-highlight: #2DD4BF; + /* Teal-400 */ + + /* Links / code links */ + --pst-color-inline-code-links: #0D9488; + /* Teal-600 */ + --pst-color-link: var(--pst-color-primary); + --pst-color-link-hover: #4338CA; + /* Indigo-700 */ + + /* Semantic */ + --pst-color-info: var(--pst-color-secondary); + --pst-color-info-highlight: var(--pst-color-secondary); + --pst-color-info-bg: #D1FAE5; + /* Teal-50 */ + --pst-color-attention: #F59E0B; + /* Amber-500 */ + --pst-color-target: #EEF2FF; + /* Indigo-50 */ + + /* Text */ + --pst-color-text-base: #1F2937; + /* Slate-800 */ + --pst-color-text-muted: #6B7280; + /* Slate-500 */ + + /* Surfaces */ + --pst-color-background: #FFFFFF; + --pst-color-on-background: #FFFFFF; + --pst-color-surface: #F3F4F6; + /* Gray-100 */ + --pst-color-on-surface: #E5E7EB; + /* Gray-200 */ + --pst-color-shadow: #D1D5DB; + --pst-color-border: #E5E7EB; + + /* Inline code */ + --pst-color-inline-code: #0D9488; + /* Teal-600 */ + + /* Tables / hovers */ + --pst-color-table-row-hover-bg: #EEF2FF; + /* Indigo-50 */ + + /* Accent (sparingly) */ + --pst-color-accent: #10B981; + /* Emerald-500 */ +} + +/* DARK THEME */ +html[data-theme="dark"] { + /* Brand */ + --pst-color-primary: #A5B4FC; + /* Indigo-300/200 mix for readability */ + --pst-color-secondary: #5EEAD4; + /* Teal-300 */ + --pst-color-secondary-highlight: #2DD4BF; + + /* Links / code links */ + --pst-color-inline-code-links: #93C5FD; + /* Indigo-300 */ + --pst-color-link: var(--pst-color-primary); + --pst-color-link-hover: #818CF8; + /* Indigo-400 */ + + /* Semantic */ + --pst-color-info: var(--pst-color-secondary); + --pst-color-info-highlight: var(--pst-color-secondary); + --pst-color-info-bg: #042F2E; + /* Deep teal */ + --pst-color-attention: #F59E0B; + --pst-color-target: #1B1C2A; + /* Indigo-tinted surface */ + + /* Text */ + --pst-color-text-base: #E5E7EB; + /* Gray-200 */ + --pst-color-text-muted: #9CA3AF; + /* Gray-400 */ + + /* Surfaces */ + --pst-color-background: #0B0C10; + /* Deep graphite */ + --pst-color-on-background: #12131A; + --pst-color-surface: #111827; + /* Slate-900 */ + --pst-color-on-surface: #1F2937; + /* Slate-800 */ + --pst-color-shadow: #0F172A; + --pst-color-border: #2A2D3A; + + /* Inline code */ + --pst-color-inline-code: #5EEAD4; + /* Teal-300 */ + + /* Tables / hovers */ + --pst-color-table-row-hover-bg: #1B1C2A; + + /* Accent */ + --pst-color-accent: #34D399; + /* Emerald-400 */ +} + +/* General tweaks */ +a { + text-decoration: none !important; +} + +.bd-header-announcement a, +.bd-header-version-warning a { + color: #5EEAD4; +} + +.form-control { + border-radius: 0 !important; + border: none !important; + outline: none !important; +} + +.navbar-brand, +.navbar-icon-links { + padding-top: 0rem !important; + padding-bottom: 0rem !important; +} + +/* Version switcher */ +.sidebar-version-switcher { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 1rem; + margin-bottom: 0.5rem; +} + +.sidebar-version-label { + font-size: 0.8rem; + font-weight: 600; + color: var(--pst-color-text-muted); + white-space: nowrap; +} + +.sidebar-version-select { + flex: 1; + font-size: 0.8rem; + padding: 0.25rem 0.5rem; + border: 1px solid var(--pst-color-border); + border-radius: 4px; + background: var(--pst-color-background); + color: var(--pst-color-text-base); + cursor: pointer; +} + +.sidebar-version-select:hover { + border-color: var(--pst-color-primary); +} + +/* Sidebar section spacing */ +.bd-sidebar .navbar-icon-links { + padding: 0 1rem 0.25rem !important; +} \ No newline at end of file diff --git a/docs/source/_static/dr_combined_rand.gif b/docs/source/_static/dr_combined_rand.gif new file mode 100644 index 0000000..6b1a99f Binary files /dev/null and b/docs/source/_static/dr_combined_rand.gif differ diff --git a/docs/source/_static/dr_pseudo_inertia.gif b/docs/source/_static/dr_pseudo_inertia.gif new file mode 100644 index 0000000..fa6f5be Binary files /dev/null and b/docs/source/_static/dr_pseudo_inertia.gif differ diff --git a/docs/source/_static/favicon.ico b/docs/source/_static/favicon.ico new file mode 100644 index 0000000..2f9ce5c Binary files /dev/null and b/docs/source/_static/favicon.ico differ diff --git a/docs/source/_static/ghost_visualization.png b/docs/source/_static/ghost_visualization.png new file mode 100644 index 0000000..370877b Binary files /dev/null and b/docs/source/_static/ghost_visualization.png differ diff --git a/docs/source/_static/mjlab-banner.jpg b/docs/source/_static/mjlab-banner.jpg new file mode 100644 index 0000000..eed1fc7 Binary files /dev/null and b/docs/source/_static/mjlab-banner.jpg differ diff --git a/docs/source/_static/native_viewer.png b/docs/source/_static/native_viewer.png new file mode 100644 index 0000000..b99d592 Binary files /dev/null and b/docs/source/_static/native_viewer.png differ diff --git a/docs/source/_static/pattern_grid.jpg b/docs/source/_static/pattern_grid.jpg new file mode 100644 index 0000000..a259ad1 Binary files /dev/null and b/docs/source/_static/pattern_grid.jpg differ diff --git a/docs/source/_static/pattern_grid.mp4 b/docs/source/_static/pattern_grid.mp4 new file mode 100644 index 0000000..65d4ae8 Binary files /dev/null and b/docs/source/_static/pattern_grid.mp4 differ diff --git a/docs/source/_static/pattern_pinhole.mp4 b/docs/source/_static/pattern_pinhole.mp4 new file mode 100644 index 0000000..10c12f0 Binary files /dev/null and b/docs/source/_static/pattern_pinhole.mp4 differ diff --git a/docs/source/_static/ray_alignment_comparison.mp4 b/docs/source/_static/ray_alignment_comparison.mp4 new file mode 100644 index 0000000..97c30b1 Binary files /dev/null and b/docs/source/_static/ray_alignment_comparison.mp4 differ diff --git a/docs/source/_static/raycast_demo.mp4 b/docs/source/_static/raycast_demo.mp4 new file mode 100644 index 0000000..c0ba6a2 Binary files /dev/null and b/docs/source/_static/raycast_demo.mp4 differ diff --git a/docs/source/_static/refs.bib b/docs/source/_static/refs.bib new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/_static/terrains/box_flat.png b/docs/source/_static/terrains/box_flat.png new file mode 100644 index 0000000..c5ef0f1 Binary files /dev/null and b/docs/source/_static/terrains/box_flat.png differ diff --git a/docs/source/_static/terrains/box_inverted_pyramid_stairs.png b/docs/source/_static/terrains/box_inverted_pyramid_stairs.png new file mode 100644 index 0000000..c0f4495 Binary files /dev/null and b/docs/source/_static/terrains/box_inverted_pyramid_stairs.png differ diff --git a/docs/source/_static/terrains/box_narrow_beams.png b/docs/source/_static/terrains/box_narrow_beams.png new file mode 100644 index 0000000..f88f87f Binary files /dev/null and b/docs/source/_static/terrains/box_narrow_beams.png differ diff --git a/docs/source/_static/terrains/box_nested_rings.png b/docs/source/_static/terrains/box_nested_rings.png new file mode 100644 index 0000000..0c38834 Binary files /dev/null and b/docs/source/_static/terrains/box_nested_rings.png differ diff --git a/docs/source/_static/terrains/box_open_stairs.png b/docs/source/_static/terrains/box_open_stairs.png new file mode 100644 index 0000000..9eff84d Binary files /dev/null and b/docs/source/_static/terrains/box_open_stairs.png differ diff --git a/docs/source/_static/terrains/box_pyramid_stairs.png b/docs/source/_static/terrains/box_pyramid_stairs.png new file mode 100644 index 0000000..c4d3eff Binary files /dev/null and b/docs/source/_static/terrains/box_pyramid_stairs.png differ diff --git a/docs/source/_static/terrains/box_random_grid.png b/docs/source/_static/terrains/box_random_grid.png new file mode 100644 index 0000000..681176f Binary files /dev/null and b/docs/source/_static/terrains/box_random_grid.png differ diff --git a/docs/source/_static/terrains/box_random_spread.png b/docs/source/_static/terrains/box_random_spread.png new file mode 100644 index 0000000..746edb5 Binary files /dev/null and b/docs/source/_static/terrains/box_random_spread.png differ diff --git a/docs/source/_static/terrains/box_random_stairs.png b/docs/source/_static/terrains/box_random_stairs.png new file mode 100644 index 0000000..0b47f77 Binary files /dev/null and b/docs/source/_static/terrains/box_random_stairs.png differ diff --git a/docs/source/_static/terrains/box_stepping_stones.png b/docs/source/_static/terrains/box_stepping_stones.png new file mode 100644 index 0000000..cf127a0 Binary files /dev/null and b/docs/source/_static/terrains/box_stepping_stones.png differ diff --git a/docs/source/_static/terrains/box_tilted_grid.png b/docs/source/_static/terrains/box_tilted_grid.png new file mode 100644 index 0000000..385a576 Binary files /dev/null and b/docs/source/_static/terrains/box_tilted_grid.png differ diff --git a/docs/source/_static/terrains/flat_patch_group.png b/docs/source/_static/terrains/flat_patch_group.png new file mode 100644 index 0000000..60a38d3 Binary files /dev/null and b/docs/source/_static/terrains/flat_patch_group.png differ diff --git a/docs/source/_static/terrains/hf_discrete_obstacles.png b/docs/source/_static/terrains/hf_discrete_obstacles.png new file mode 100644 index 0000000..6d63726 Binary files /dev/null and b/docs/source/_static/terrains/hf_discrete_obstacles.png differ diff --git a/docs/source/_static/terrains/hf_perlin_noise.png b/docs/source/_static/terrains/hf_perlin_noise.png new file mode 100644 index 0000000..15183f7 Binary files /dev/null and b/docs/source/_static/terrains/hf_perlin_noise.png differ diff --git a/docs/source/_static/terrains/hf_pyramid_slope.png b/docs/source/_static/terrains/hf_pyramid_slope.png new file mode 100644 index 0000000..8866224 Binary files /dev/null and b/docs/source/_static/terrains/hf_pyramid_slope.png differ diff --git a/docs/source/_static/terrains/hf_random_uniform.png b/docs/source/_static/terrains/hf_random_uniform.png new file mode 100644 index 0000000..38dabbe Binary files /dev/null and b/docs/source/_static/terrains/hf_random_uniform.png differ diff --git a/docs/source/_static/terrains/hf_wave.png b/docs/source/_static/terrains/hf_wave.png new file mode 100644 index 0000000..d936b23 Binary files /dev/null and b/docs/source/_static/terrains/hf_wave.png differ diff --git a/docs/source/_static/tutorials/cartpole_swingup.mp4 b/docs/source/_static/tutorials/cartpole_swingup.mp4 new file mode 100644 index 0000000..e5a43b5 Binary files /dev/null and b/docs/source/_static/tutorials/cartpole_swingup.mp4 differ diff --git a/docs/source/_static/tutorials/cartpole_training_curve.png b/docs/source/_static/tutorials/cartpole_training_curve.png new file mode 100644 index 0000000..0b1b3dd Binary files /dev/null and b/docs/source/_static/tutorials/cartpole_training_curve.png differ diff --git a/docs/source/_static/viser_camera_pane.png b/docs/source/_static/viser_camera_pane.png new file mode 100644 index 0000000..dd7f3a3 Binary files /dev/null and b/docs/source/_static/viser_camera_pane.png differ diff --git a/docs/source/_static/viser_viewer.png b/docs/source/_static/viser_viewer.png new file mode 100644 index 0000000..4fed515 Binary files /dev/null and b/docs/source/_static/viser_viewer.png differ diff --git a/docs/source/actions.rst b/docs/source/actions.rst new file mode 100644 index 0000000..3e8b6ec --- /dev/null +++ b/docs/source/actions.rst @@ -0,0 +1,173 @@ +.. _actions: + +Actions +======= + +Actions define how the policy controls the simulation. The action +manager receives the policy's output tensor each step, splits it across +registered action terms, and routes each slice to the appropriate +entity's actuators. Each term maps a contiguous segment of the policy +output to a control mode (position, velocity, effort) on a set of +joints, tendons, or sites. + +.. code-block:: python + + from mjlab.envs.mdp.actions import JointPositionActionCfg + + actions = { + "joint_pos": JointPositionActionCfg( + entity_name="robot", + actuator_names=(".*",), # regex matching actuator names + scale=0.5, + use_default_offset=True, # action 0 = default pose + ), + } + + +Common parameters +----------------- + +All action types share a base set of parameters inherited from +``BaseActionCfg``. + +``entity_name`` identifies the scene entity to control. ``actuator_names`` +is a tuple of regex patterns matched against actuator (or tendon/site) +names to select the controlled targets. + +``scale`` multiplies the raw policy output before any offset is applied. +It accepts a scalar or a dict mapping actuator name patterns to +per-target values. This keeps policy outputs in a normalized range while +mapping to physically meaningful units. ``offset`` is added after +scaling; joint action types also provide ``use_default_offset``, which +automatically loads the entity's default joint positions or velocities +as the offset so that a raw output of zero produces the default pose. + +``clip`` optionally clamps the processed action (after scale and offset) +before it reaches the actuator. It accepts a dict mapping actuator name +patterns to ``(min, max)`` tuples, resolved the same way as ``scale`` +and ``offset``. + +.. code-block:: python + + JointPositionActionCfg( + entity_name="robot", + actuator_names=(".*",), + scale=0.5, + clip={".*_hip_.*": (-1.0, 1.0), ".*_knee_.*": (-0.5, 2.0)}, + ) + +Actions are written to actuator targets on every decimation substep +(physics step), not just once per policy step. This is in contrast to +observation delay, which operates in units of policy steps. + + +Action types +------------ + +.. list-table:: + :header-rows: 1 + :widths: 28 72 + + * - Type + - Description + * - ``JointPositionAction`` + - Sets joint position targets. With ``use_default_offset=True`` + (the default), a policy output of zero commands the default pose. + Encoder bias from ``dr.encoder_bias`` is subtracted automatically + so that randomized offsets propagate correctly to the control + command. + * - ``RelativeJointPositionAction`` + - Sets joint position targets relative to the current joint positions. + The target is ``current_pos + action * scale``, so a policy output of + zero holds the robot in place regardless of its current configuration. + * - ``JointVelocityAction`` + - Sets joint velocity targets. ``use_default_offset=True`` uses the + default joint velocities (typically zero). + * - ``JointEffortAction`` + - Sets joint effort (torque) targets directly. No default offset. + * - ``TendonLengthAction`` + - Sets tendon length targets. Targets are resolved by matching + ``actuator_names`` against tendon names. + * - ``TendonVelocityAction`` + - Sets tendon velocity targets. + * - ``TendonEffortAction`` + - Sets tendon effort targets. + * - ``SiteEffortAction`` + - Applies forces and torques at named sites. Useful for + quadrotors and drones where thrust is applied at rotor sites + rather than through joint actuators. + + +Task-space actions +------------------ + +``DifferentialIKAction`` converts Cartesian position and orientation +commands into joint-space position targets via damped least-squares +inverse kinematics. One IK step is executed per decimation substep, so +the end-effector tracks the target continuously across substeps rather +than only at policy frequency. + +The action dimension is selected automatically based on configuration: + +- ``orientation_weight == 0``: **3D** (position only) +- ``orientation_weight > 0, use_relative_mode=True``: **6D** (delta + position + delta axis-angle) +- ``orientation_weight > 0, use_relative_mode=False``: **7D** (absolute + position + quaternion) + +All objectives (position, orientation, joint limits, posture) are +stacked into a single DLS system. Setting a weight to zero disables +that objective with no overhead in the solve. + +The ``compute_dq()`` method returns joint displacements without writing +to actuator targets, enabling multi-iteration IK in standalone scripts +outside of RL training. + + +Action dimensions and history +------------------------------ + +The total action dimension presented to the policy is the sum of each +registered term's ``action_dim``. For joint, tendon, and site actions +this equals the number of matched targets. For ``DifferentialIKAction`` +it is 3, 6, or 7 depending on the active objectives. + +The action manager tracks the three most recent action vectors: +``action``, ``prev_action``, and ``prev_prev_action``. Observation terms +such as ``last_action`` and reward terms such as ``action_rate_l2`` and +``action_acc_l2`` read from these buffers. Action history is zeroed on +environment reset so that episode boundaries do not leak information. + + +Multiple action terms +--------------------- + +An environment can register any number of terms. The action manager +concatenates their dimensions in registration order, splits the +policy's output tensor at the corresponding boundaries, and routes +each slice independently. + +.. code-block:: python + + from mjlab.envs.mdp.actions import ( + JointPositionActionCfg, + JointVelocityActionCfg, + ) + + actions = { + "arm_joints": JointPositionActionCfg( + entity_name="robot", + actuator_names=(".*_arm_.*",), + scale=0.5, + ), + "wheel_joints": JointVelocityActionCfg( + entity_name="robot", + actuator_names=(".*_wheel_.*",), + scale=10.0, + ), + } + +The policy outputs a tensor whose width equals the total number of +matched targets across all terms. Terms can also target different +entities, for example one term for a robot and another for an object +being manipulated. diff --git a/docs/source/actuators.rst b/docs/source/actuators.rst new file mode 100644 index 0000000..4227837 --- /dev/null +++ b/docs/source/actuators.rst @@ -0,0 +1,476 @@ +.. _actuators: + +Actuators +========= + +Actuators convert high-level commands (position, velocity, effort) into +low-level efforts that drive joints. They are configured through the +``articulation`` field of :ref:`EntityCfg `. mjlab provides +**built-in** actuators that leverage the physics engine's implicit +integration for best stability, and **explicit** actuators for custom +control laws and actuator dynamics. + + +Quick start +----------- + +Basic PD control with ``BuiltinPositionActuator``, the most common +starting point. + +.. code-block:: python + + from mjlab.actuator import BuiltinPositionActuatorCfg + from mjlab.entity import EntityCfg, EntityArticulationInfoCfg + + robot_cfg = EntityCfg( + spec_fn=lambda: load_robot_spec(), + articulation=EntityArticulationInfoCfg( + actuators=( + BuiltinPositionActuatorCfg( + target_names_expr=(".*_hip_.*", ".*_knee_.*"), + stiffness=80.0, + damping=10.0, + effort_limit=100.0, + ), + ), + ), + ) + +Add delay fields directly on any actuator config to model communication +latency. + +.. code-block:: python + + from mjlab.actuator import BuiltinPositionActuatorCfg + + BuiltinPositionActuatorCfg( + target_names_expr=(".*",), + stiffness=80.0, + damping=10.0, + delay_min_lag=2, # Minimum 2 physics steps + delay_max_lag=5, # Maximum 5 physics steps + ) + + +Built-in vs explicit actuators +------------------------------ + +The key design decision when configuring actuators is whether to use +**built-in** or **explicit** types. The difference comes down to how +MuJoCo's integrator handles velocity-dependent forces. + +**Built-in actuators** (``BuiltinPositionActuator``, +``BuiltinVelocityActuator``, ``BuiltinMotorActuator``, +``BuiltinPdActuator``, ``BuiltinDcMotorActuator``, +``BuiltinMuscleActuator``) create native MuJoCo actuator elements in the +MjSpec. The physics engine computes the control law and integrates +velocity-dependent damping forces implicitly. This provides the best +numerical stability, particularly with high gains or large timesteps. + +**Explicit actuators** (``IdealPdActuator``, ``DcMotorActuator``, +``LearnedMlpActuator``) compute torques in user code and forward them +through a ```` actuator acting as a passthrough. Because the +integrator cannot account for the velocity derivatives of these +externally computed forces, they are less numerically robust than built-in +types. Use explicit actuators when you need custom control laws or actuator +dynamics that cannot be expressed with built-in types (e.g., +velocity-dependent torque limits, learned actuator networks). + +The two approaches match closely in the linear, unconstrained regime at +small timesteps. At larger timesteps or higher gains, built-in actuators +are more forgiving. + +**Integrator choice.** mjlab places damping inside the actuator rather than +in joints. The ``euler`` integrator treats joint damping implicitly but +actuator damping explicitly, limiting stability. The ``implicitfast`` +integrator treats all known velocity-dependent forces implicitly, handling +both proportional and damping terms of the actuator without additional cost. + +.. note:: + + mjlab defaults to ``implicitfast``, as it is MuJoCo's recommended + integrator and provides superior stability for actuator-side damping. + + +Actuator types +-------------- + +All actuator configs share a few common fields inherited from +``ActuatorCfg``: + +- ``target_names_expr``: Tuple of regex patterns matched against joint + names (or tendon/site names when using a different + ``transmission_type``). +- ``armature``: Reflected rotor inertia added to the target joint. +- ``frictionloss``: Static friction (stiction) modeled as a constraint + on the target joint. See MuJoCo's + `frictionloss `_. + +Built-in actuators +^^^^^^^^^^^^^^^^^^ + +Built-in actuators use MuJoCo's native actuator types via the MjSpec API. + +**BuiltinPositionActuator**: Creates ```` actuators for PD +control. + +**BuiltinVelocityActuator**: Creates ```` actuators for velocity +control. + +**BuiltinMotorActuator**: Creates ```` actuators for direct torque +control. + +**BuiltinPdActuator**: Native PD that closes on both a position and a +velocity target, implemented as paired ```` + ```` +actuators summing to ``kp * (p_target - q) + kd * (v_target - qdot)``. +``BuiltinPositionActuator`` puts kd on the ```` element and +implicitly assumes a zero velocity reference; use this when the policy +emits a non-zero velocity target. Native delivery lets +``implicit`` / ``implicitfast`` see the kd term in their velocity update, +unlike ``IdealPdActuator`` which forwards Python-computed torque through +an opaque ````. + +**BuiltinDcMotorActuator**: Wraps MuJoCo's native +` `_ +element. Torque is ``tau = K * (V - K * omega) / R``; the back-EMF runs +through the native bias path, so ``implicit`` / ``implicitfast`` pick up +its velocity derivative as effective damping. Three input modes pick what +``ctrl`` carries: VOLTAGE drives the motor directly; POSITION / VELOCITY +close an internal PID (with anti-windup and slew limiting) against a +single setpoint, whose Vmax-clamped output becomes torque. POSITION mode +pins v_target = 0 (the kd term acts on raw velocity). Optional physics: +inductance, +thermal model with I^2R heating, cogging ripple, LuGre friction. +``DcMotorActuator`` (the explicit version) is a software PD with a +velocity-dependent torque clamp on top of a ````; this is the real +electrical model. + +**BuiltinMuscleActuator**: Creates ```` actuators for +biologically-inspired muscle dynamics with force-length-velocity +characteristics. + +.. code-block:: python + + from mjlab.actuator import BuiltinPositionActuatorCfg, BuiltinVelocityActuatorCfg + + # Mobile manipulator: PD for arm joints, velocity control for wheels. + actuators = ( + BuiltinPositionActuatorCfg( + target_names_expr=(".*_shoulder_.*", ".*_elbow_.*", ".*_wrist_.*"), + stiffness=100.0, + damping=10.0, + effort_limit=150.0, + ), + BuiltinVelocityActuatorCfg( + target_names_expr=(".*_wheel_.*",), + damping=20.0, + effort_limit=50.0, + ), + ) + + +Explicit actuators +^^^^^^^^^^^^^^^^^^ + +Explicit actuators compute efforts and forward them to an underlying +```` actuator acting as a passthrough. See +`Built-in vs explicit actuators`_ above for stability implications. + +**IdealPdActuator**: Implements an ideal PD controller. Computes torques +as ``tau = Kp * pos_error + Kd * vel_error``. + +**DcMotorActuator**: Extends ``IdealPdActuator`` with velocity-dependent +torque saturation to model DC motor torque-speed curves (back-EMF +effects). Implements a linear torque-speed curve: maximum torque at zero +velocity, zero torque at maximum velocity. + +**LearnedMlpActuator**: Neural network-based actuator that uses a +trained MLP to predict torque outputs from joint state history. Useful +when analytical models cannot capture complex actuator dynamics like +delays, nonlinearities, and friction effects. Inherits DC motor +velocity-based torque limits. + +.. code-block:: python + + from mjlab.actuator import IdealPdActuatorCfg, DcMotorActuatorCfg + + # Ideal PD for hips, DC motor model with torque-speed curve for knees. + actuators = ( + IdealPdActuatorCfg( + target_names_expr=(".*_hip_.*",), + stiffness=80.0, + damping=10.0, + effort_limit=100.0, + ), + DcMotorActuatorCfg( + target_names_expr=(".*_knee_.*",), + stiffness=80.0, + damping=10.0, + effort_limit=25.0, # Continuous torque limit + saturation_effort=50.0, # Peak torque at stall + velocity_limit=30.0, # No-load speed (rad/s) + ), + ) + + +XML actuators +^^^^^^^^^^^^^ + +XML actuators wrap actuators already defined in your robot's XML file. The +config finds existing actuators by matching their ``target`` joint name +against the ``target_names_expr`` patterns. Each joint must have exactly one +matching actuator. + +**XmlActuator**: Wraps any actuator already defined in the XML. The +actuator type (position, velocity, motor, muscle) is auto detected from +the XML element, or you can set ``command_field`` explicitly. + +.. code-block:: python + + from mjlab.actuator import XmlActuatorCfg + + # Robot XML already has: + # + # + # + + # Wrap existing XML actuators. + actuators = ( + XmlActuatorCfg(target_names_expr=("hip_joint",)), + ) + +Actuator delays +^^^^^^^^^^^^^^^ + +Any actuator config supports inline delay fields for modeling command +latency. On a real robot, the onboard PD loop runs at KHz with direct +encoder access, but the position target from the policy arrives late due +to inference time and communication bus cycles. Actuator +delay models this: the command target is delayed, but the control law +still sees fresh joint state. + +This is distinct from observation delay, which models sensor pipeline +latency (stale state going into the policy). Together they cover both +legs of the round trip: sensor to policy to motor. + +.. code-block:: python + + from mjlab.actuator import IdealPdActuatorCfg + + # Add 2-5 step delay to position commands. + actuators = ( + IdealPdActuatorCfg( + target_names_expr=(".*",), + stiffness=80.0, + damping=10.0, + delay_min_lag=2, + delay_max_lag=5, + delay_hold_prob=0.3, # 30% chance to keep current lag + delay_update_period=10, # Resample lag every 10 steps + ), + ) + +Each step, a lag is sampled uniformly from ``[delay_min_lag, +delay_max_lag]``. Delays are quantized to physics timesteps. For +example, with 500Hz physics (2ms/step), ``delay_min_lag=2`` represents +a 4ms minimum delay. + + +Authoring actuator configs +-------------------------- + +Since actuator parameters are uniform within each config, use separate +actuator configs for joints that need different parameters: + +.. code-block:: python + + from mjlab.actuator import BuiltinPositionActuatorCfg + + # G1 humanoid with different gains per joint group. + G1_ACTUATORS = ( + BuiltinPositionActuatorCfg( + target_names_expr=(".*_hip_.*", "waist_yaw_joint"), + stiffness=180.0, + damping=18.0, + effort_limit=88.0, + armature=0.0015, + ), + BuiltinPositionActuatorCfg( + target_names_expr=("left_hip_pitch_joint", "right_hip_pitch_joint"), + stiffness=200.0, + damping=20.0, + effort_limit=88.0, + armature=0.0015, + ), + BuiltinPositionActuatorCfg( + target_names_expr=(".*_knee_joint",), + stiffness=150.0, + damping=15.0, + effort_limit=139.0, + armature=0.0025, + ), + BuiltinPositionActuatorCfg( + target_names_expr=(".*_ankle_.*",), + stiffness=40.0, + damping=5.0, + effort_limit=25.0, + armature=0.0008, + ), + ) + +This design choice reflects a deliberate simplification in mjlab: each +``ActuatorCfg`` represents a single actuator type (e.g., a specific +motor/gearbox model) applied uniformly across all joints it drives. +Hardware parameters such as ``armature`` (reflected rotor inertia) and +``gear`` describe properties of the actuator hardware, even though they +are implemented in MuJoCo as joint or actuator fields. In other frameworks +(like Isaac Lab), these fields may accept ``float | dict[str, float]`` to +support per-joint variation. mjlab instead encourages one config per +actuator type or per joint group, keeping the hardware model physically +consistent and explicit. The main trade-off is verbosity in special cases, +such as parallel linkages, where per-joint overrides could have been +convenient, but the benefit is clearer semantics and simpler maintenance. + +See :ref:`actions` for how action terms route policy outputs to actuators +(including DifferentialIK for task-space control), and +:ref:`domain_randomization` for randomizing gains and effort limits. + + +Computing hardware parameters +------------------------------ + +This section is relevant when configuring actuators from real motor +datasheets. If you are using manually tuned gains, you can skip ahead. + +mjlab provides utilities in ``mjlab.utils.actuator`` to compute actuator +parameters from physical motor specifications. This is particularly +useful for computing reflected inertia (``armature``) and deriving +appropriate control gains from hardware datasheets. + +**Example: Unitree G1 motor configuration** + +.. code-block:: python + + from math import pi + + from mjlab.utils.actuator import ( + reflected_inertia_from_two_stage_planetary, + ElectricActuator + ) + + # Motor specs from manufacturer datasheet. + ROTOR_INERTIAS_7520_14 = ( + 0.489e-4, # Motor rotor inertia (kg*m**2) + 0.098e-4, # Planet carrier inertia + 0.533e-4, # Output stage inertia + ) + GEARS_7520_14 = ( + 1, # First stage (motor to planet) + 4.5, # Second stage (planet to carrier) + 1 + (48/22), # Third stage (carrier to output) + ) + + # Compute reflected inertia at joint output. + # J_reflected = J_motor*(N1*N2)**2 + J_carrier*N2**2 + J_output. + ARMATURE_7520_14 = reflected_inertia_from_two_stage_planetary( + ROTOR_INERTIAS_7520_14, GEARS_7520_14 + ) + + # Create motor spec container. + ACTUATOR_7520_14 = ElectricActuator( + reflected_inertia=ARMATURE_7520_14, + velocity_limit=32.0, # rad/s at joint + effort_limit=88.0, # N*m continuous torque + ) + + # Derive PD gains from natural frequency and damping ratio. + NATURAL_FREQ = 10 * 2*pi # 10 Hz bandwidth. + DAMPING_RATIO = 2.0 # Overdamped, see note below. + STIFFNESS = ARMATURE_7520_14 * NATURAL_FREQ**2 + DAMPING = 2 * DAMPING_RATIO * ARMATURE_7520_14 * NATURAL_FREQ + + # Use in actuator config. + from mjlab.actuator import BuiltinPositionActuatorCfg + + actuator = BuiltinPositionActuatorCfg( + target_names_expr=(".*_hip_pitch_joint",), + stiffness=STIFFNESS, + damping=DAMPING, + effort_limit=ACTUATOR_7520_14.effort_limit, + armature=ACTUATOR_7520_14.reflected_inertia, + ) + +.. note:: + + The example uses ``DAMPING_RATIO = 2.0`` + (overdamped) rather than the critically damped value of 1.0. This is + because the reflected inertia calculation only accounts for the motor's + rotor inertia, not the apparent inertia of the links being moved. In + practice, the total effective inertia at the joint is higher than just + the reflected motor inertia, so using an overdamped ratio provides + better stability margins when the true system inertia is + underestimated. + +**Parallel linkage approximation:** + +For joints driven by parallel linkages (like the G1's ankles with dual +motors), the effective armature in the nominal configuration can be +approximated as the sum of the individual motor armatures: + +.. code-block:: python + + # Two 5020 motors driving ankle through parallel linkage. + G1_ACTUATOR_ANKLE = BuiltinPositionActuatorCfg( + target_names_expr=(".*_ankle_pitch_joint", ".*_ankle_roll_joint"), + stiffness=STIFFNESS_5020 * 2, + damping=DAMPING_5020 * 2, + effort_limit=ACTUATOR_5020.effort_limit * 2, + armature=ACTUATOR_5020.reflected_inertia * 2, + ) + + +Extending: custom actuators +---------------------------- + +All actuators implement a unified ``compute()`` interface that receives an +``ActuatorCmd`` (containing position, velocity, and effort targets) and +returns control signals for the low-level MuJoCo actuators driving each +joint. + +**Core interface:** + +.. code-block:: python + + def compute(self, cmd: ActuatorCmd) -> torch.Tensor: + """Convert high-level commands to control signals. + + Args: + cmd: Command containing position_target, velocity_target, + effort_target (each is a [num_envs, num_targets] tensor + or None) + + Returns: + Control signals for this actuator + ([num_envs, num_targets] tensor) + """ + +**Lifecycle hooks:** + +- ``edit_spec``: Modify MjSpec before compilation (add actuators, set + gains) +- ``initialize``: Post-compilation setup (resolve indices, allocate + buffers) +- ``reset``: Per-environment reset logic +- ``update``: Pre-step updates +- ``compute``: Convert commands to control signals + +**Properties:** + +- ``target_ids``: Tensor of local target indices controlled by this + actuator +- ``target_names``: List of target names controlled by this actuator +- ``ctrl_ids``: Tensor of global control input indices for this actuator + +``IdealPdActuator`` is the recommended base class for custom explicit +actuators. ``DcMotorActuator`` and ``LearnedMlpActuator`` are both +built on top of it and serve as examples of the extension pattern. diff --git a/docs/source/api/actuator.rst b/docs/source/api/actuator.rst new file mode 100644 index 0000000..f89063b --- /dev/null +++ b/docs/source/api/actuator.rst @@ -0,0 +1,182 @@ +mjlab.actuator +============== + +.. automodule:: mjlab.actuator + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`Actuator` + - :class:`ActuatorCfg` + - :class:`ActuatorCmd` + - :class:`BuiltinActuatorGroup` + - :class:`BuiltinMotorActuator` + - :class:`BuiltinMotorActuatorCfg` + - :class:`BuiltinPositionActuator` + - :class:`BuiltinPositionActuatorCfg` + - :class:`BuiltinVelocityActuator` + - :class:`BuiltinVelocityActuatorCfg` + - :class:`BuiltinPdActuator` + - :class:`BuiltinPdActuatorCfg` + - :class:`BuiltinDcMotorActuator` + - :class:`BuiltinDcMotorActuatorCfg` + - :class:`DcMotorInputMode` + - :class:`DcMotorDatasheetParams` + - :class:`DcMotorPhysicalParams` + - :class:`BuiltinMuscleActuator` + - :class:`BuiltinMuscleActuatorCfg` + - :class:`XmlActuator` + - :class:`XmlActuatorCfg` + - :class:`IdealPdActuator` + - :class:`IdealPdActuatorCfg` + - :class:`DcMotorActuator` + - :class:`DcMotorActuatorCfg` + - :class:`LearnedMlpActuator` + - :class:`LearnedMlpActuatorCfg` + +Base +---- + +.. autoclass:: Actuator + :members: + :show-inheritance: + +.. autoclass:: ActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: ActuatorCmd + :members: + :exclude-members: __init__ + :undoc-members: + +Builtin Actuators +----------------- + +.. autoclass:: BuiltinActuatorGroup + :members: + :show-inheritance: + +.. autoclass:: BuiltinMotorActuator + :members: + :show-inheritance: + +.. autoclass:: BuiltinMotorActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: BuiltinPositionActuator + :members: + :show-inheritance: + +.. autoclass:: BuiltinPositionActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: BuiltinVelocityActuator + :members: + :show-inheritance: + +.. autoclass:: BuiltinVelocityActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: BuiltinPdActuator + :members: + :show-inheritance: + +.. autoclass:: BuiltinPdActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: BuiltinDcMotorActuator + :members: + :show-inheritance: + +.. autoclass:: BuiltinDcMotorActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + +.. autoclass:: DcMotorInputMode + :members: + :show-inheritance: + +.. autoclass:: DcMotorDatasheetParams + :members: + :exclude-members: __init__ + :undoc-members: + +.. autoclass:: DcMotorPhysicalParams + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: BuiltinMuscleActuator + :members: + :show-inheritance: + +.. autoclass:: BuiltinMuscleActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + +XML Actuators +------------- + +.. autoclass:: XmlActuator + :members: + :show-inheritance: + +.. autoclass:: XmlActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Ideal PD Actuator +----------------- + +.. autoclass:: IdealPdActuator + :members: + :show-inheritance: + +.. autoclass:: IdealPdActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + +DC Motor Actuator +----------------- + +.. autoclass:: DcMotorActuator + :members: + :show-inheritance: + +.. autoclass:: DcMotorActuatorCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Learned MLP Actuator +-------------------- + +.. autoclass:: LearnedMlpActuator + :members: + :show-inheritance: + +.. autoclass:: LearnedMlpActuatorCfg + :members: + :exclude-members: __init__ diff --git a/docs/source/api/entity.rst b/docs/source/api/entity.rst new file mode 100644 index 0000000..d6e6e5d --- /dev/null +++ b/docs/source/api/entity.rst @@ -0,0 +1,45 @@ +mjlab.entity +============ + +.. automodule:: mjlab.entity + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`Entity` + - :class:`EntityCfg` + - :class:`EntityArticulationInfoCfg` + - :class:`EntityIndexing` + - :class:`EntityData` + +Entity +------ + +.. autoclass:: Entity + :members: + :show-inheritance: + +.. autoclass:: EntityCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: EntityArticulationInfoCfg + :members: + :exclude-members: __init__ + :undoc-members: + +EntityIndexing +-------------- + +.. autoclass:: EntityIndexing + :members: + +EntityData +---------- + +.. autoclass:: EntityData + :members: diff --git a/docs/source/api/envs.rst b/docs/source/api/envs.rst new file mode 100644 index 0000000..4dc4257 --- /dev/null +++ b/docs/source/api/envs.rst @@ -0,0 +1,36 @@ +mjlab.envs +========== + +.. automodule:: mjlab.envs + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`ManagerBasedRlEnv` + - :class:`ManagerBasedRlEnvCfg` + - :data:`VecEnvObs` + - :data:`VecEnvStepReturn` + +ManagerBasedRlEnv +----------------- + +.. autoclass:: ManagerBasedRlEnv + :members: + :show-inheritance: + +.. autoclass:: ManagerBasedRlEnvCfg + :members: + :exclude-members: __init__ + :undoc-members: + +VecEnvObs +--------- + +.. autodata:: VecEnvObs + +VecEnvStepReturn +---------------- + +.. autodata:: VecEnvStepReturn diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst new file mode 100644 index 0000000..9db9efb --- /dev/null +++ b/docs/source/api/index.rst @@ -0,0 +1,19 @@ +API Reference +============= + +This section provides detailed API documentation for all public modules in mjlab. + +.. toctree:: + :maxdepth: 1 + + envs + scene + sim + entity + actuator + sensor + managers + terrains + rl + viewer + tasks diff --git a/docs/source/api/managers.rst b/docs/source/api/managers.rst new file mode 100644 index 0000000..8852919 --- /dev/null +++ b/docs/source/api/managers.rst @@ -0,0 +1,208 @@ +mjlab.managers +============== + +.. automodule:: mjlab.managers + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`ManagerBase` + - :class:`ManagerTermBase` + - :class:`ManagerTermBaseCfg` + - :class:`SceneEntityCfg` + - :class:`ActionManager` + - :class:`ActionTerm` + - :class:`ActionTermCfg` + - :class:`ObservationManager` + - :class:`ObservationGroupCfg` + - :class:`ObservationTermCfg` + - :class:`RewardManager` + - :class:`RewardTermCfg` + - :class:`TerminationManager` + - :class:`TerminationTermCfg` + - :class:`CommandManager` + - :class:`NullCommandManager` + - :class:`CommandTerm` + - :class:`CommandTermCfg` + - :class:`CurriculumManager` + - :class:`NullCurriculumManager` + - :class:`CurriculumTermCfg` + - :class:`EventManager` + - :class:`EventMode` + - :class:`EventTermCfg` + - :class:`MetricsManager` + - :class:`NullMetricsManager` + - :class:`MetricsTermCfg` + - :class:`RecorderManager` + - :class:`NullRecorderManager` + - :class:`RecorderTerm` + - :class:`RecorderTermCfg` + +Base +---- + +.. autoclass:: ManagerBase + :members: + :show-inheritance: + +.. autoclass:: ManagerTermBase + :members: + :show-inheritance: + +.. autoclass:: ManagerTermBaseCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: SceneEntityCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Action Manager +-------------- + +.. autoclass:: ActionManager + :members: + :show-inheritance: + +.. autoclass:: ActionTerm + :members: + :show-inheritance: + +.. autoclass:: ActionTermCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Observation Manager +------------------- + +.. autoclass:: ObservationManager + :members: + :show-inheritance: + +.. autoclass:: ObservationGroupCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: ObservationTermCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Reward Manager +-------------- + +.. autoclass:: RewardManager + :members: + :show-inheritance: + +.. autoclass:: RewardTermCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Termination Manager +------------------- + +.. autoclass:: TerminationManager + :members: + :show-inheritance: + +.. autoclass:: TerminationTermCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Command Manager +--------------- + +.. autoclass:: CommandManager + :members: + :show-inheritance: + +.. autoclass:: NullCommandManager + :members: + :show-inheritance: + +.. autoclass:: CommandTerm + :members: + :show-inheritance: + +.. autoclass:: CommandTermCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Curriculum Manager +------------------ + +.. autoclass:: CurriculumManager + :members: + :show-inheritance: + +.. autoclass:: NullCurriculumManager + :members: + :show-inheritance: + +.. autoclass:: CurriculumTermCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Event Manager +------------- + +.. autoclass:: EventManager + :members: + :show-inheritance: + +.. autoclass:: EventMode + :members: + :undoc-members: + +.. autoclass:: EventTermCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Metrics Manager +--------------- + +.. autoclass:: MetricsManager + :members: + :show-inheritance: + +.. autoclass:: NullMetricsManager + :members: + :show-inheritance: + +.. autoclass:: MetricsTermCfg + :members: + :exclude-members: __init__ + +Recorder Manager +---------------- + +.. autoclass:: RecorderManager + :members: + :show-inheritance: + +.. autoclass:: NullRecorderManager + :members: + :show-inheritance: + +.. autoclass:: RecorderTerm + :members: + :show-inheritance: + +.. autoclass:: RecorderTermCfg + :members: + :exclude-members: __init__ + :undoc-members: diff --git a/docs/source/api/rl.rst b/docs/source/api/rl.rst new file mode 100644 index 0000000..2f173a3 --- /dev/null +++ b/docs/source/api/rl.rst @@ -0,0 +1,52 @@ +mjlab.rl +======== + +.. automodule:: mjlab.rl + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`MjlabOnPolicyRunner` + - :class:`RslRlVecEnvWrapper` + - :class:`RslRlOnPolicyRunnerCfg` + - :class:`RslRlPpoAlgorithmCfg` + - :class:`RslRlModelCfg` + - :class:`RslRlBaseRunnerCfg` + +Runner +------ + +.. autoclass:: MjlabOnPolicyRunner + :members: + :show-inheritance: + +.. autoclass:: RslRlVecEnvWrapper + :members: + :show-inheritance: + +Configuration +------------- + +.. autoclass:: RslRlOnPolicyRunnerCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: RslRlPpoAlgorithmCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: RslRlModelCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: RslRlBaseRunnerCfg + :members: + :exclude-members: __init__ diff --git a/docs/source/api/scene.rst b/docs/source/api/scene.rst new file mode 100644 index 0000000..5d5f983 --- /dev/null +++ b/docs/source/api/scene.rst @@ -0,0 +1,23 @@ +mjlab.scene +=========== + +.. automodule:: mjlab.scene + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`Scene` + - :class:`SceneCfg` + +Scene +----- + +.. autoclass:: Scene + :members: + +.. autoclass:: SceneCfg + :members: + :exclude-members: __init__ + :undoc-members: diff --git a/docs/source/api/sensor.rst b/docs/source/api/sensor.rst new file mode 100644 index 0000000..70edd71 --- /dev/null +++ b/docs/source/api/sensor.rst @@ -0,0 +1,126 @@ +mjlab.sensor +============ + +.. automodule:: mjlab.sensor + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`Sensor` + - :class:`SensorCfg` + - :class:`SensorContext` + - :class:`BuiltinSensor` + - :class:`BuiltinSensorCfg` + - :class:`ObjRef` + - :class:`ContactSensor` + - :class:`ContactSensorCfg` + - :class:`ContactData` + - :class:`ContactMatch` + - :class:`RayCastSensor` + - :class:`RayCastSensorCfg` + - :class:`RayCastData` + - :class:`GridPatternCfg` + - :class:`PinholeCameraPatternCfg` + - :class:`CameraSensor` + - :class:`CameraSensorCfg` + - :class:`CameraSensorData` + +Base +---- + +.. autoclass:: Sensor + :members: + :show-inheritance: + +.. autoclass:: SensorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: SensorContext + :members: + +Builtin Sensor +-------------- + +.. autoclass:: BuiltinSensor + :members: + :show-inheritance: + +.. autoclass:: BuiltinSensorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: ObjRef + :members: + :exclude-members: __init__ + :undoc-members: + +Contact Sensor +-------------- + +.. autoclass:: ContactSensor + :members: + :show-inheritance: + +.. autoclass:: ContactSensorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: ContactData + :members: + +.. autoclass:: ContactMatch + :members: + :exclude-members: __init__ + :undoc-members: + +Ray Cast Sensor +--------------- + +.. autoclass:: RayCastSensor + :members: + :show-inheritance: + +.. autoclass:: RayCastSensorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: RayCastData + :members: + +.. autoclass:: GridPatternCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: PinholeCameraPatternCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Camera Sensor +------------- + +.. autoclass:: CameraSensor + :members: + :show-inheritance: + +.. autoclass:: CameraSensorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: CameraSensorData + :members: diff --git a/docs/source/api/sim.rst b/docs/source/api/sim.rst new file mode 100644 index 0000000..f3faa6b --- /dev/null +++ b/docs/source/api/sim.rst @@ -0,0 +1,44 @@ +mjlab.sim +========= + +.. automodule:: mjlab.sim + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`Simulation` + - :class:`SimulationCfg` + - :class:`MujocoCfg` + - :class:`TorchArray` + - :class:`WarpBridge` + +Simulation +---------- + +.. autoclass:: Simulation + :members: + +.. autoclass:: SimulationCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: MujocoCfg + :members: + :exclude-members: __init__ + :undoc-members: + +TorchArray +---------- + +.. autoclass:: TorchArray + :members: + +WarpBridge +---------- + +.. autoclass:: WarpBridge + :members: diff --git a/docs/source/api/tasks.rst b/docs/source/api/tasks.rst new file mode 100644 index 0000000..4b9dd8b --- /dev/null +++ b/docs/source/api/tasks.rst @@ -0,0 +1,25 @@ +mjlab.tasks +=========== + +.. automodule:: mjlab.tasks.registry + +.. rubric:: Functions + +.. hlist:: + :columns: 3 + + - :func:`register_mjlab_task` + - :func:`list_tasks` + - :func:`load_env_cfg` + - :func:`load_rl_cfg` + - :func:`load_runner_cls` + +.. autofunction:: register_mjlab_task + +.. autofunction:: list_tasks + +.. autofunction:: load_env_cfg + +.. autofunction:: load_rl_cfg + +.. autofunction:: load_runner_cls diff --git a/docs/source/api/terrains.rst b/docs/source/api/terrains.rst new file mode 100644 index 0000000..976e394 --- /dev/null +++ b/docs/source/api/terrains.rst @@ -0,0 +1,167 @@ +mjlab.terrains +============== + +.. automodule:: mjlab.terrains + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`TerrainEntity` + - :class:`TerrainEntityCfg` + - :class:`TerrainGenerator` + - :class:`TerrainGeneratorCfg` + - :class:`SubTerrainCfg` + - :class:`FlatPatchSamplingCfg` + - :class:`HfDiscreteObstaclesTerrainCfg` + - :class:`HfPerlinNoiseTerrainCfg` + - :class:`HfPyramidSlopedTerrainCfg` + - :class:`HfRandomUniformTerrainCfg` + - :class:`HfWaveTerrainCfg` + - :class:`BoxFlatTerrainCfg` + - :class:`BoxInvertedPyramidStairsTerrainCfg` + - :class:`BoxNarrowBeamsTerrainCfg` + - :class:`BoxNestedRingsTerrainCfg` + - :class:`BoxOpenStairsTerrainCfg` + - :class:`BoxPyramidStairsTerrainCfg` + - :class:`BoxRandomGridTerrainCfg` + - :class:`BoxRandomSpreadTerrainCfg` + - :class:`BoxRandomStairsTerrainCfg` + - :class:`BoxSteppingStonesTerrainCfg` + - :class:`BoxTiltedGridTerrainCfg` + +Core +---- + +.. autoclass:: TerrainEntity + :members: + :show-inheritance: + +.. autoclass:: TerrainEntityCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: TerrainGenerator + :members: + +.. autoclass:: TerrainGeneratorCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: SubTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + + +.. autoclass:: FlatPatchSamplingCfg + :members: + :exclude-members: __init__ + :undoc-members: + +Heightfield Terrains +-------------------- + +.. autoclass:: HfDiscreteObstaclesTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: HfPerlinNoiseTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: HfRandomUniformTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: HfPyramidSlopedTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: HfWaveTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +Primitive (Box) Terrains +------------------------ + +.. autoclass:: BoxFlatTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxInvertedPyramidStairsTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxNarrowBeamsTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxNestedRingsTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxOpenStairsTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxPyramidStairsTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxRandomGridTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxRandomSpreadTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxRandomStairsTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxSteppingStonesTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: + +.. autoclass:: BoxTiltedGridTerrainCfg + :members: + :exclude-members: __init__ + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/viewer.rst b/docs/source/api/viewer.rst new file mode 100644 index 0000000..21c6570 --- /dev/null +++ b/docs/source/api/viewer.rst @@ -0,0 +1,72 @@ +mjlab.viewer +============ + +.. automodule:: mjlab.viewer + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + - :class:`ViewerConfig` + - :class:`BaseViewer` + - :class:`NativeMujocoViewer` + - :class:`ViserPlayViewer` + - :class:`OffscreenRenderer` + +.. rubric:: Protocols + +.. hlist:: + :columns: 3 + + - :class:`EnvProtocol` + - :class:`PolicyProtocol` + - :class:`VerbosityLevel` + +ViewerConfig +------------ + +.. autoclass:: ViewerConfig + :members: + :exclude-members: __init__ + :undoc-members: + +BaseViewer +---------- + +.. autoclass:: BaseViewer + :members: + :show-inheritance: + +NativeMujocoViewer +------------------ + +.. autoclass:: NativeMujocoViewer + :members: + :show-inheritance: + +ViserPlayViewer +--------------- + +.. autoclass:: ViserPlayViewer + :members: + :show-inheritance: + +OffscreenRenderer +----------------- + +.. autoclass:: OffscreenRenderer + :members: + :show-inheritance: + +Protocols +--------- + +.. autoclass:: EnvProtocol + :members: + +.. autoclass:: PolicyProtocol + :members: + +.. autoclass:: VerbosityLevel + :members: diff --git a/docs/source/architecture_overview.rst b/docs/source/architecture_overview.rst new file mode 100644 index 0000000..ea5dd12 --- /dev/null +++ b/docs/source/architecture_overview.rst @@ -0,0 +1,192 @@ +.. _architecture_overview: + +Architecture Overview +===================== + +mjlab is organized into two layers: a **simulation layer** that models +the robot and world, and a **manager layer** that defines the +reinforcement learning problem on top of it. Understanding this separation +is the fastest way to build a mental map of the system. + +.. figure:: _static/architecture_diagram.png + :width: 60% + :align: center + :alt: mjlab architecture diagram + + Entities are composed into an MjSpec, compiled, and transferred to + MuJoCo Warp for GPU simulation. The ManagerBasedRlEnv orchestrates the + MDP; RSL-RL handles training. + + +The simulation layer +-------------------- + +**Scene pipeline.** +mjlab constructs scenes by composing entity descriptions into a single +`MjSpec `_. +Each entity starts from an +`MJCF `_ file +loaded via ``MjSpec.from_file()``. Users who define everything in XML can +use this directly. For more control, Python dataclasses can extend or +override properties on the loaded spec: actuators, collision rules, +materials, sensors, and initial state. This hybrid approach lets users +start from existing MuJoCo models and layer on task-specific configuration +without modifying the original XML. The composed specification is compiled +into an ``MjModel`` on the CPU, then transferred to the GPU via +`MuJoCo Warp `_, +which is built on `NVIDIA Warp `_. + +**MuJoCo Warp.** +MuJoCo Warp is a GPU-accelerated backend for MuJoCo. It preserves +MuJoCo's ``MjModel``/``MjData`` paradigm but adds a leading *world* +dimension: a single ``MjData`` object holds the state of N independent +simulation instances in parallel, enabling thousands of environments to +be stepped simultaneously. Model parameters are shared across all worlds +by default, and individual fields can be expanded to vary per-world when +domain randomization requires it. mjlab captures the simulation step as a +`CUDA graph `_: the kernel +execution sequence is recorded once and replayed on subsequent calls, +eliminating CPU-side dispatch overhead. + +.. note:: + + CUDA graph capture is a one-time cost at environment startup. Per-episode + resets and domain randomization events run as regular Python between graph + replays and do not break the capture. + +**Components.** +The simulation layer provides four core components, each with its own +documentation page: + +- :ref:`entity`: a robot, a manipulated object, or a static object such + as :ref:`terrain `, defined by an MJCF description plus + optional Python configuration for actuators, collision rules, and + initial state. +- :ref:`actuators`: how entities are controlled. Users can wrap actuators + already defined in MJCF or create new ones from Python configuration. +- :ref:`sensors`: how the world is observed. Includes MuJoCo-native + sensors as well as custom sensors like RGB-D cameras and raycasters. +- :ref:`scene`: scene composition and environment placement. + + +The manager layer +----------------- + +On top of the simulation layer, mjlab adopts the manager-based environment +design introduced by Isaac Lab. Users define their environment by composing +small, self-contained *terms* (reward functions, observation computations, +domain randomization events) and register them with the appropriate manager. +Each manager handles the lifecycle of its terms: calling them at the right +point in the simulation loop, aggregating their outputs, and exposing +diagnostics. + +Terms can be plain functions for stateless computations, or classes that +inherit from ``ManagerTermBase`` when they need to cache expensive setup +(such as resolving regex patterns to joint indices at initialization) or +maintain per-episode state through a ``reset()`` hook. + +Environments are configured through ``ManagerBasedRlEnvCfg``, a plain +dataclass that holds term configuration dictionaries for each manager. + +.. code-block:: python + + from mjlab.envs import ManagerBasedRlEnvCfg + + cfg = ManagerBasedRlEnvCfg( + decimation=4, # 4 physics steps per policy step + episode_length_s=20.0, + scene=..., # SceneCfg: terrain, entities, sensors + sim=..., # SimulationCfg: timestep, solver, integrator + observations={...}, # ObservationManager terms + actions={...}, # ActionManager terms + rewards={...}, # RewardManager terms + terminations={...}, # TerminationManager terms + events={...}, # EventManager terms (resets, DR) + commands={...}, # CommandManager terms (velocity targets, etc.) + curriculum={...}, # CurriculumManager terms + metrics={...}, # MetricsManager terms + ) + +.. rubric:: The eight managers + +- **ObservationManager**: assembles observation groups with configurable + processing (clipping, noise, delay, history). Supports asymmetric + actor-critic. See :ref:`observations`. +- **ActionManager**: routes the policy's output tensor to entity actuators, + handling scaling and offset. See :ref:`actions`. +- **RewardManager**: computes a weighted sum of reward terms, scaled by step + duration for frequency invariance. See :ref:`rewards`. +- **TerminationManager**: evaluates stop conditions, distinguishing terminal + resets from timeouts. See :ref:`terminations`. +- **EventManager**: fires terms at lifecycle points (startup, reset, + interval). Domain randomization is implemented through event terms. + See :ref:`events` and :ref:`domain_randomization`. +- **CommandManager**: generates and resamples goal signals (velocity + targets, pose targets). See :ref:`commands`. +- **CurriculumManager**: adjusts training conditions based on policy + performance. See :ref:`curriculum`. +- **MetricsManager**: logs custom per-step values as episode averages. + See :ref:`metrics`. + +For the full configuration reference covering all managers, see +:ref:`environment_config`. + + +The environment lifecycle +------------------------- + +Each environment instance passes through four phases. + +1. **Build.** ``Scene`` composes entity MJCF files via ``MjSpec`` and + compiles ``MjModel`` on the CPU. ``Simulation`` uploads the model to the + GPU via MuJoCo Warp, allocating a single ``MjData`` with N parallel + worlds. CUDA graphs for ``step``, ``forward``, ``reset``, and ``sense`` + are captured. + +2. **Initialize.** Managers are constructed from the term configuration + dictionaries. Regex patterns are matched to joint, body, and geom + indices. Observation history and delay buffers are allocated. Model + fields required by domain randomization terms are expanded from shared + to per-world storage, and CUDA graphs are rebuilt to reflect the new + layout. Startup events are fired once. + +3. **Reset.** Called at the start of training and whenever an environment + terminates or times out. The ``EventManager`` fires ``reset`` terms, + which return the scene to an initial state with optional randomization. + Command targets are resampled. Observation history buffers are cleared. + +4. **Step.** The policy action is processed by the ``ActionManager``. The + physics simulation advances ``decimation`` times, with actuator commands + applied and entity state updated each sub-step. After the decimation + loop, the ``TerminationManager`` checks stop conditions and the + ``RewardManager`` computes the reward signal. Step and interval events + fire if scheduled, acting on the pre-reset state. Any terminated + environments are then reset. A single ``forward()`` call refreshes + derived quantities for all environments. The ``CommandManager`` advances + or resamples goals. Sensors update. The ``ObservationManager`` assembles + the observation for the next policy query. + +The step sequence in order: + +.. code-block:: text + + action_manager.process_action(action) + for _ in range(decimation): + action_manager.apply_action() + sim.step() + scene.update() + termination_manager.compute() + reward_manager.compute() + metrics_manager.compute() + event_manager.apply(mode="step") + event_manager.apply(mode="interval") + [reset terminated envs] + sim.forward() + command_manager.compute() # dt=0 for envs just reset + sim.sense() + observation_manager.compute() + +With this mental model in place, the Concepts pages cover each simulation +layer component in detail, and The Manager Layer pages walk through each +manager's configuration and built-in terms. If you are coming from Isaac +Lab, :ref:`migration_isaac_lab` describes the key API differences. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 0000000..0b8b643 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,1044 @@ +========= +Changelog +========= + +Upcoming version (not yet released) +----------------------------------- + +Changed +^^^^^^^ + +- Bumped ``rsl-rl-lib`` from 5.4.2 to 5.5.0. This update removes the ``logger_type`` + attribute of the ``rsl_rl.utils.Logger``, so code that previously checked + ``logger.logger_type`` must instead check the type of ``logger.writer``. + +Version 1.6.0 (August 8, 2026) +------------------------------ + +.. admonition:: Breaking API changes + :class: attention + + - ``CollisionCfg`` now requires ``contype``, ``conaffinity``, ``condim``, + and ``priority`` to be explicit instead of silently defaulting to + MuJoCo's values, and dict values for these fields must cover every + matched geom (add a catch-all ``".*"`` entry). + - ``CommandTerm._update_command`` now takes an ``env_ids`` argument: + ``None`` on the regular per-step update and the reset environment ids + when called from ``reset()``. Custom command terms must add the + parameter (construction raises a ``TypeError`` with migration + instructions otherwise) and scope any per-step state advance, such as + a motion frame index, to ``env_ids``. + - ``ViewerConfig`` is now keyword-only; positional construction no + longer works. + +.. admonition:: Highlights + :class: note + + - Upgraded to MuJoCo and MuJoCo Warp 3.11. + - Upgraded ``rsl-rl-lib`` to 5.4.2. + +Added +^^^^^ + +- Added ``GeomCfg``, exposed as the ``geoms`` field on ``EntityCfg``, a spec + editor that matches geoms by name and patches their attributes. Supports + ``group`` (so a geom can collide without being drawn) and all collision + attributes; unset attributes are left untouched. Contribution by + @bd-pmorais. +- Added ``diffuse``, ``specular``, ``ambient``, ``active``, and + ``attenuation`` fields to ``LightCfg`` for configuring light color and + falloff. Contribution by @bd-pmorais. +- Added ``random``, ``file``, ``cubefiles``, ``gridsize``, ``gridlayout``, + ``nchannel``, ``hflip``, and ``vflip`` fields to ``TextureCfg``, so textures + can be loaded from image files instead of only built-in patterns. + ``width`` and ``height`` are now optional, since file-based textures take + their size from the image. Contribution by @bd-pmorais. +- Added light domain randomization functions: ``dr.light_diffuse``, + ``dr.light_specular``, ``dr.light_ambient``, ``dr.light_attenuation``, + ``dr.light_cutoff``, and ``dr.light_exponent``. Contribution by @bd-pmorais. +- Added ``reduce="sum"`` to ``MetricsTermCfg`` for reporting the accumulated + episode total (e.g. episodic reward, total distance traveled) instead of a + per-step average. Contribution by @bd-mlutter +- Added ``ViewerConfig.geom_group`` and ``ViewerConfig.site_group`` to + control which geom and site visualization groups the offscreen renderer + draws. Defaults match MuJoCo's (groups 0 through 2), so rendering is + unchanged unless configured. Contribution by @bd-mlutter. +- Added ``dr.mat_texid`` to randomize which texture fills a given + ``mjtTextureRole`` slot (RGB by default) of each selected material, + sampling uniformly from ``asset_cfg.texture_names``. Contribution by + @bd-pmorais. + +.. figure:: _static/changelog/mat_texid_dr.gif + :width: 30% + +Changed +^^^^^^^ + +- Bumped ``mujoco`` and ``mujoco-warp`` from 3.10 to 3.11, and regenerated the + bundled MuJoCo type stubs. +- Bumped ``rsl-rl-lib`` from 5.4.0 to 5.4.2. +- ``CollisionCfg`` and ``GeomCfg`` now share one write path, and mjlab warns + when a ``GeomCfg`` collision patch is overwritten by a ``CollisionCfg``. +- Changed the default MuJoCo Warp render background to solid black + (``0, 0, 0, 1``), matching MuJoCo's native renderer. Contribution by + @bd-pmorais. +- The offscreen renderer now works on a copy of the ``MjModel``, so its + render-only tweaks (extent, shadows, reflections, offscreen size) no + longer leak into the shared model. Contribution by @bd-mlutter. +- ``ViewerConfig`` is now keyword-only, with fields grouped and documented. + Contribution by @bd-mlutter. +- ``ViewerConfig.fovy`` now also applies to the ``ASSET_ROOT`` and + ``ASSET_BODY`` tracking cameras instead of being silently ignored; leave + it at ``None`` (the default) to keep the model value. Contribution by + @bd-mlutter. +- ``auto_reset`` and an explicit ``reset()`` now leave identical command and + event timer state (:issue:`1133`). + +Fixed +^^^^^ + +- The Viser motion scrubber's Start Here button no longer computes relative + body poses from stale pre-scrub kinematics, which could spuriously + terminate the episode on the next step. +- Mid-episode lifting command resamples now refresh kinematics and the + multi-cube reward cache, so observations and rewards no longer see + pre-teleport object positions for one step after each resample. +- ``UniformVelocityCommand``'s ``init_velocity_prob`` path no longer writes + the previous episode's terminal pose back into the sim on reset. It read + derived kinematics before ``forward()`` ran and rewrote the root pose; it + now reads only qpos for the orientation and writes only the root velocity. +- ``init_velocity_prob`` now applies on episode reset only. A mid-episode + timer resample used to also teleport the root velocity, which ran after + ``step()``'s forward and left velocity observations stale for one step. +- ``Entity.set_joint_position_target`` and its velocity/effort/tendon/site + siblings now select the outer product when both ``env_ids`` and the element + ids are tensors, instead of pairing them elementwise. +- ``MotionCommand`` now refreshes kinematics after a timer-expiry resample + (finite ``resampling_time_range``), matching its wraparound path. +- ``CircularBuffer`` lag retrieval now clamps to the oldest retained frame; + a lag beyond the buffer length used to wrap around to a newer frame. +- Camera sensor caches are now invalidated after ``sense()``, so a + pre-sense read with ``clone_data=True`` can no longer pin the previous + step's frame into the observations (mirrors the raycast fix for + :issue:`998`). +- ``reset(env_ids=...)`` no longer appends a frame to every env's observation + history and delay buffers; only the reset envs receive their post-reset + frame. Previously, each manual partial reset gave the other envs a duplicate + frame, shortening their effective history and drifting their delay + schedules. +- ``reset(env_ids=...)`` no longer advances stateful commands in + environments that were not reset. Previously a partial reset gave every + environment an extra command update, so with ``auto_reset=False`` a + ``MotionCommand`` reference motion played at twice the normal speed and + could teleport non-reset robots via the wraparound resample. The adaptive + sampling EMA also no longer folds on resets, matching auto-reset + training dynamics. :issue:`1138` +- Interval event timers are now resampled on episode reset for function-based + terms, as documented. Previously the countdown carried across episodes, so a + new episode's first ``push_robot`` in the velocity and tracking tasks could + fire arbitrarily soon after spawn; with mixed function and class interval + terms, reset also wrote into the wrong timer slots. Note this changes push + timing relative to earlier training runs, and an interval term whose range + exceeds the episode length is now re-armed on every reset and will never + fire. +- ``RayCastSensorCfg.include_geom_groups`` now raises on values outside + ``[0, mjNGROUP)`` instead of silently excluding every geom. +- Geoms with a negative group no longer pick up group 5's visibility toggle in + the Viser viewer. + +Version 1.5.3 (July 22, 2026) +----------------------------- + +Changed +^^^^^^^ + +- The Viser reward bar panel's term cap is now configurable via + ``ViewerConfig.reward_bar_max_terms``, so environments with more than 20 + reward terms can show them all. Defaults to 20, preserving previous behavior. + :issue:`1079` + +Fixed +^^^^^ + +- Bumped ``pillow`` (12.3.0), ``onnx`` (1.22.0), and ``soupsieve`` (2.9.1) in the + lockfile to pick up security fixes. +- Fixed raycast sensor debug visualization and observations lagging one step + behind the sensed hits. ``sense()`` rebinds the hit tensors after the cache had + already been repopulated by a pre-sense reward read, so ``.data`` returned the + previous step's hits; the cache is now invalidated after ``postprocess_rays``. + With ``ray_alignment="yaw"`` this made debug rays appear tilted by the foot's + per-step motion instead of vertical. :issue:`998` +- Restored ONNX uploads and W&B run metadata for velocity and manipulation + training when using RSL-RL's current ``WandbLogWriter`` logger name. +- The Viser reward bar panel no longer *silently* drops reward terms beyond + ``max_terms``; it now emits a warning listing the hidden terms. Previously + environments with more than 20 reward terms had the overflow disappear from + the bar panel with no indication. :issue:`1079` +- Fixed the ``terrain_levels_vel`` curriculum promoting every env from level 0 + to level 1 on the initial reset, ignoring ``max_init_terrain_level=0``. Before + the first step the robot sits at its spawn pose rather than a walked-to + position, so the distance check was spurious; terrain levels are now frozen on + that first reset. :issue:`1094` +- Fixed the velocity task's actor ``joint_pos`` observation not being biased by + the ``encoder_bias`` domain randomization, so the encoder bias only affected + actions and never the observed joint positions. The actor now observes biased + joint positions while the critic keeps the true (unbiased) values as + privileged information, matching the tracking task. + See `discussion #1065 `_. +- Hardened ``fit_terrain_normal`` against non-finite raycast hits. A single env + with a diverged state produced a NaN/Inf covariance that made + ``torch.linalg.eigh`` raise and abort the whole batch; such rows now fall back + to the up vector. This stops the hard crash so a diverged env can be reset + normally; it does not by itself make a diverged env's downstream reward finite. + :issue:`912` +- Enabled ``obs_normalization`` on the Go1 velocity actor and critic to match + the other velocity tasks. Without it, extreme-but-finite observations on rough + terrain drove value/policy divergence that eventually surfaced as a + ``normal expects all elements of std >= 0.0`` crash. Note that Go1 velocity + checkpoints trained before this change carry no normalizer buffers and will no + longer load; retrain from scratch. :issue:`870` :issue:`1044` :issue:`1053` +- Fixed ``ContactSensor`` air-time tracking accumulating float32 sim-clock + differences, whose quantization error grows with the clock magnitude and made + ``compute_first_contact`` / ``compute_first_air`` miss touchdowns on long runs. + The exact float64 substep ``dt`` is now accumulated instead. :issue:`1101` +- Bumped ``mujoco-warp`` to 3.10.0.3, fixing a CUDA 700 illegal memory access in + ``smooth.crb`` triggered by startup mass domain randomization (via + ``set_const``) once ``num_envs >= 128`` on consumer Ada GPUs. :issue:`1108` + +Version 1.5.2 (July 17, 2026) +----------------------------- + +Fixed +^^^^^ + +- Fixed CUDA illegal memory accesses when domain randomization triggers + ``set_const`` with multiple environments. ``actuator_acc0`` is now expanded + per environment before MuJoCo Warp recomputes it. +- Fixed ``MaterialCfg.reflectance`` being ignored when building the MuJoCo + spec. Contribution by @bd-pmorais. + +Version 1.5.1 (July 15, 2026) +----------------------------- + +Added +^^^^^ + +- Added ``MeshCfg``, a spec editor that matches mesh assets by name and edits + their asset-level attributes. The first attribute is ``maxhullvert``, which + caps the collision convex hull's vertex count to lower narrowphase cost. +- Added ``SimulationCfg.broadphase`` and ``SimulationCfg.broadphase_filter`` + to configure MuJoCo Warp's broadphase collision algorithm and + bounding-volume filters. + +Changed +^^^^^^^ + +- Enabled skybox rendering for camera sensors. Contribution by @bd-pmorais. +- Bumped the minimum ``mujoco-warp`` to 3.10.0.2, which fixes ``qfrc_constraint`` + being populated incorrectly across vectorized environments (:issue:`1086`). + Earlier 3.10.0.x releases are no longer supported. +- Command delay on fusable actuators (ideal PD, DC motor) now applies one shared + lag per environment across all fused actuators sharing a delay config, matching + the built-in actuator path, rather than an independent lag per actuator group + (:issue:`1035`). + +Fixed +^^^^^ + +- Fixed ``TerrainGenerator`` overwriting custom geom names set by sub-terrain + functions with the default ``terrain_{i}`` name. Only unnamed geoms are now + auto-named. +- Fixed ``TorchArray`` not expanding world-shared model fields to ``nworld`` + with mujoco_warp 3.10.0.2, which allocates them as real size-1 arrays + instead of stride-0 broadcast views. Multi-env indexing of fields like + ``soft_joint_pos_limits`` raised ``IndexError`` during resets (:issue:`1093`). +- Fixed ``mdp.bad_orientation`` returning NaN when float32 rounding in + ``quat_apply_inverse`` pushed the projected-gravity z-component slightly + outside ``[-1, 1]``, making ``torch.acos`` return NaN and silently + suppressing the termination for flipped robots. The argument is now clamped + to ``[-1, 1]``. +- Fixed a crash when using command delay on ideal PD (or other custom) + actuators whenever ``num_envs`` differed from the number of delayed targets, + and fused ideal PD and DC motor actuators sharing a transmission and delay + config into a single gather, delay, control-law evaluation, and control + write, removing per-group host overhead (:issue:`1035`). + +Version 1.5.0 (June 28, 2026) +----------------------------- + +Added +^^^^^ + +- Added ``reduce="max"`` to ``MetricsTermCfg`` for reporting episode-peak values + (e.g. peak power, peak contact force) without needing stateful wrapper classes. +- Added ``BuiltinDcMotorActuator``, a native MuJoCo ```` wrapper. + Supports voltage / position / velocity input modes with back-EMF, + configurable motor constants, and optional integral, slew, inductance, + thermal, LuGre, and cogging extensions. +- Added ``scale_with_difficulty`` to ``HfRandomUniformTerrainCfg``. When + enabled, the noise amplitude scales with difficulty (flat at 0, full + ``noise_range`` at 1) so the terrain progresses in a curriculum. Defaults to + ``False``, preserving the previous difficulty-independent behavior. +- Added material domain randomization functions for MuJoCo Warp RGB rendering: + ``dr.mat_emission``, ``dr.mat_specular``, ``dr.mat_shininess``, and + ``dr.mat_texrepeat``. + +Changed +^^^^^^^ + +- Bumped ``rsl-rl-lib`` from 5.2.0 to 5.4.0. +- Bumped ``mujoco`` and ``mujoco-warp`` to 3.10, both pinned from PyPI. The + ``py.mujoco.org`` nightly index and the ``mujoco-warp`` git pin are dropped, so + resolution no longer breaks when nightly wheels are garbage-collected. + + .. warning:: + + ``SimulationCfg.ls_parallel`` is deprecated and now ignored, since parallel + linesearch was removed upstream in MuJoCo Warp. Setting it emits a + ``DeprecationWarning``; remove it from any ``SimulationCfg`` you construct. +- Curriculum-mode terrain difficulty is now deterministic across rows + and reaches the configured ``difficulty_range`` endpoints + (:issue:`1027`). +- Heightfield terrains now color by absolute height with a diverging palette + (cool below the ground plane, green at ground level, warm above) on a fixed + scale, replacing the per-patch normalization. Color is now consistent across + terrains, and low-amplitude terrain such as ``random_rough`` reads as gently + tinted ground instead of high-contrast noise. +- ``BoxNestedRingsTerrainCfg`` now builds uniform-height concentric ridges + whose separating gaps widen with difficulty, replacing the random per-ring + heights. Rings are colored by height (like the other terrains) and the outer + border matches the ring height. +- Terrain generation no longer prints timing information to stdout. + +Fixed +^^^^^ + +- Fixed domain randomization events that target different ``axes`` of the same + model field (e.g. two ``dr.geom_size`` events scaling axis 0 and axis 1 + separately) silently clobbering each other. Each event now writes back only + the axes it targeted, so per-axis events compose (:issue:`1042`). +- Regenerated the bundled MuJoCo type stubs, which had drifted from the + installed mujoco version. CI now regenerates them and fails if they are + stale, so they stay in sync going forward. Run ``make stubs`` to update them + (:issue:`1048`). +- Fixed ``select_gpus`` crashing when ``CUDA_VISIBLE_DEVICES`` contains MIG + UUIDs instead of numeric indices. +- Fixed pyramid-stairs terrains (``BoxPyramidStairsTerrainCfg``, + ``BoxInvertedPyramidStairsTerrainCfg``, and ``BoxOpenStairsTerrainCfg``) + leaving an empty, geometry-free border at difficulty 0, where the step + height collapses to zero. The flat border frame is now always generated as + solid geometry flush with the ground (:issue:`1033`). +- Fixed ``HfPerlinNoiseTerrainCfg`` failing to compile at difficulty 0, where + the target height collapses to zero and MuJoCo rejects the non-positive + heightfield size. +- Fixed ``BoxRandomGridTerrainCfg`` producing NaN colors (and failing to build) + at difficulty 0, where the grid height is zero and the color normalization + divided by zero. +- Fixed the center platform z-fighting with surrounding geometry in + ``BoxRandomGridTerrainCfg`` (grid cells were left underneath the platform) and + ``BoxRandomSpreadTerrainCfg`` (the platform duplicated the floor surface). +- Fixed ``BoxNarrowBeamsTerrainCfg`` square platform corners protruding between + the beams at high difficulty; the platform now shrinks to stay within the + beams' angular coverage. +- Fixed ``BoxSteppingStonesTerrainCfg`` reconfiguring abruptly at a difficulty + threshold, where the stone grid re-tiled as its spacing crossed an integer + boundary, and leaving an oversized gap around the center platform. The grid is + now difficulty-independent and the platform snaps to it as a clean island. +- Fixed ``train --video``, ``play``, and ``demo`` crashing with ``OpenGL + platform library not loaded`` on headless Linux hosts that don't pre-set + ``MUJOCO_GL``. The default is now applied in ``mjlab/__init__.py`` (Linux + only) so it takes effect before mujoco's GL backend selection runs. +- Fixed motion tracking re-anchoring to a stale robot pose after a mid-episode + motion resample. ``MotionCommand._update_command`` now calls ``sim.forward()`` + after resampling so relative body poses read the post-teleport state + (:issue:`1068`). + +Version 1.4.0 (May 26, 2026) +---------------------------- + +Added +^^^^^ + +- Added ``BuiltinPdActuator``, the implicit-integration version of + ``IdealPdActuator``. Same interface (position + velocity targets, + kp/kd gains), but expresses the PD as native MuJoCo ```` + and ```` elements so the ``implicit`` / ``implicitfast`` + integrators include the kp/kd derivatives in their velocity update. + The actuator stays stable at gain/timestep combinations where + explicit Python PD would diverge, which matters when you want to + run a real motor's stiff on-board PD gains in sim. ``effort_limit`` + is enforced as a sum-clamp on the two PD terms via + ``jnt_actfrcrange`` (or ``tendon_actfrcrange``). Supported by + ``dr.pd_gains`` and ``dr.effort_limits``. +- Added ``mdp.projected_gravity_from_sensor``, an observation that derives + projected gravity from a ``framezaxis`` up-vector sensor (negated) rather + than from the root body orientation. Unlike ``mdp.projected_gravity``, it + reflects the sensor's site frame, so it can observe IMU mounting domain + randomization (e.g. via ``dr.site_quat``). Go1 and G1 ship an + ``imu_upvector`` sensor for this. +- Added ``DebugVisualizer.add_box`` for drawing an axis-oriented box + primitive, mirroring ``add_ellipsoid``. Supported by both the native + and Viser viewers. ``size`` is the box half-extents (:issue:`992`). +- Added ``--log-root`` CLI option to ``train``, ``play``, and ``evaluate`` + scripts for choosing where training logs are stored. Defaults to + ``logs/rsl_rl`` (unchanged behavior). Useful for directing outputs to a + scratch disk or shared mount. +- ``RewardManager``, ``TerminationManager``, and ``MetricsManager`` now + validate that every term function returns a tensor of shape + ``(num_envs,)`` when evaluated, raising a clear ``ValueError`` + naming the offending term instead of silently broadcasting or crashing + with an opaque error later during training. +- Added ``ContactSensor.primary_names`` property to expose the resolved + primary names in the order they appear along the per-contact axis of the + output tensors. This makes it possible to map a contact-data column back + to the primary it belongs to (:issue:`914`). +- Added per-world mesh variant support via ``VariantEntityCfg``. Each + world in a batched simulation can now use a different mesh asset for + the same logical entity (e.g. world 0 holds a cube, world 1 a + sphere). Variants are passed as a ``dict[str, Callable]`` of named + spec callables; the optional ``assignment`` field controls how worlds + map to variants and accepts ``None`` (uniform), a ``dict[str, float]`` + of per-variant weights, or a custom ``Callable[[int], Sequence[int]]``. + Mesh-derived constants (collision bounds, body inertials, subtree + mass, inverse weights) are compiled per-variant and stored as + per-world arrays in the Warp model, so domain randomization, the + native viewer, the offscreen renderer, and the Viser viewer all pick + up the variant assignment automatically. Variants must share the + same kinematic structure (same bodies, joints, joint types); only + mesh geoms may differ. Assignment is fixed at simulation init. See + :ref:`heterogeneous_worlds` for usage. With help from @XiangruiJiang. +- Per-world mesh variants now support per-variant materials and textures. + Each variant can reference its own named material, which is automatically + prefixed and scattered via ``geom_matid`` alongside the existing + ``geom_dataid`` table. Variants without a material get ``matid = -1``. + Contribution by @omarrayyann. +- Added ``dr.geom_matid`` to randomize which baked material each geom uses + per environment, sampling uniformly from ``asset_cfg.material_names``. + Contribution by @bd-pmorais. + +Changed +^^^^^^^ + +- ``Entity`` now raises a clear error at construction when its spec contains + more than one freejoint. An entity models a single system rooted at one + body, so it has at most one freejoint; a second one was previously accepted + silently and only surfaced later as a cryptic shape mismatch when writing + root state. Model each detached floating body as its own entry in + ``SceneCfg.entities`` instead. +- Changed ``compute_root_relative_mpkpe`` to re-anchor the reference to the + robot's root each step, removing yaw drift as well as translation so it + measures intrinsic body pose error. +- Changed ``compute_joint_velocity_error`` from an L2 norm to a per-joint + RMS, so it no longer scales with the number of joints. +- Bumped ``mujoco`` to 3.8 and ``mujoco-warp`` to 3.8.0. The ``multiccd`` + enable flag was removed in mujoco 3.8 (it became default-on), so configs + that listed ``"multiccd"`` in ``MujocoCfg.enableflags`` need to drop it. +- Camera segmentation now matches ``mujoco_warp``'s typed segmentation + output. ``CameraSensorData.segmentation`` stores ``(object_id, + object_type)`` pairs in shape ``[B, H, W, 2]`` instead of the previous + legacy geom-id-only layout. Contribution by @tkelestemur. +- Sped up ``RayCaster`` post-processing by removing boolean-mask indexing + operations and replacing them with ``masked_fill_`` plus a clamped-distance + formulation of ``hit_pos_w`` that places misses at the world origin. This + removes all CUDA syncs from the ray post-process, letting the CPU thread + proceed while GPU-based sensing runs. Contribution by @bd-pdomanico. +- Bumped ``rsl-rl-lib`` from 5.0.1 to 5.2.0. This brings ``torch.compile`` support for + PPO and Distillation, and optional std clamping and constant std in + ``GaussianDistribution``. No code changes required on the mjlab side. +- ``TerrainEntityCfg`` debug visualization sites (environment origins, + terrain origins, flat patches) are now off by default. Set + ``debug_vis=True`` to re-enable them. The sites inflated ``nsite`` and + caused a measurable slowdown in the per-step ``site_local_to_global`` + kernel (:issue:`942`). +- Task package load failures during ``mjlab`` import now print the full + traceback (and the entry point's module path) to ``stderr`` instead of + just the exception message, making it easier to pinpoint the source of + import errors when running commands like ``list-envs`` (:issue:`910`). + Contribution by @saikishor. +- Clarified ``ContactSensor`` shape conventions: per-contact fields + (``found``, ``force``, ``torque``, ``dist``, ``pos``, ``normal``, + ``tangent``) have shape ``[B, P * num_slots, ...]`` while per-primary + air-time fields (``current_air_time``, ``last_air_time``, + ``current_contact_time``, ``last_contact_time``) have shape ``[B, P]``, + where ``P`` is the number of resolved primaries (:issue:`914`). +- Event functions now share a single ``resolve_env_ids`` helper to expand + ``env_ids=None`` to all environments, replacing five copies of the same + guard. ``push_by_setting_velocity`` and ``apply_external_force_torque`` + accept ``env_ids=None`` too, so they work as global-time interval terms. + Documented when to use ``apply_external_force_torque`` (a constant, + self-managed wrench) versus ``apply_body_impulse`` (transient, automatic + impulses) versus ``push_by_setting_velocity`` (an instantaneous velocity + kick). + +Fixed +^^^^^ + +- Removed use of deprecated ``warp-lang`` symbols (``wp.context.runtime`` + and ``wp.context.Device``) that were dropped in newer ``warp-lang`` + releases, causing ``AttributeError: module 'warp' has no attribute + 'context'`` at import/runtime. mjlab now uses + ``wp.get_cuda_driver_version()`` and ``wp.Device`` instead + (:issue:`967`). Contribution by @rdeits. +- Fixed the tracking ``evaluate`` script scoring each metric against the + next motion frame; the reference is now snapshotted before each step to + match the reward. +- Fixed the tracking end-effector metrics silently scoring zero for an + unknown body name; they now raise ``ValueError``. +- Fixed ``compute_mpkpe`` measuring root-relative instead of global error; + it now uses the global reference ``body_pos_w`` (:issue:`1006`). +- Fixed heavy flicker in offscreen training videos on rough-terrain tasks. + The renderer recomputed its context "neighbor" robots every frame from + ``env_origins``, which the terrain curriculum mutates on reset, so the + neighbor set kept changing and robots popped in and out. The neighbor + set is now computed once and cached (:issue:`979`). +- Fixed command delay only applying to an actuator's position target. + ``IdealPdActuator`` and ``DcMotorActuator`` also use velocity and effort, which + arrived undelayed and out of sync; all command targets now share one delay. + Zero-reference setups are unaffected. +- Fixed duplicate random seeds across nodes in multi-node training. The + per-process seed offset in ``scripts/train.py`` now uses the global + ``RANK`` instead of ``LOCAL_RANK``. Contribution by @bd-pdomanico. +- Fixed ``apply_body_impulse`` firing an impulse on the very first step (and + the first step after every reset) instead of starting with a cooldown as + documented. The cooldown is now sampled lazily on the first call so impulse + timing is decorrelated from episode resets (:issue:`973`). +- Fixed ``dr.pd_gains`` and ``dr.effort_limits`` silently no-oping when + passed an ``Operation`` object (e.g. ``dr.scale``) instead of a string. + Both functions now accept ``Operation | str`` like every other DR event + and raise ``ValueError`` for unsupported operations (:issue:`971`). +- Fixed ``ContactSensor`` with ``global_frame=True`` and + ``reduce`` ∈ {``"none"``, ``"mindist"``, ``"maxforce"``} producing forces + rotated onto the wrong axis. The contact-frame→world rotation matrix had + its columns ordered ``[tangent, tangent2, normal]`` instead of + ``[normal, tangent, tangent2]``, projecting the normal-force component + onto a tangent direction. Contribution by @bd-pdomanico. +- Fixed ``extras["log"]`` entries written by reward terms (e.g. ``Metrics/*`` + values in velocity tasks) being silently discarded on any step where at + least one environment resets. ``_reset_idx`` was clearing the dict after + ``reward_manager.compute()`` had already populated it. The clear now + happens at the top of ``step()`` and ``reset()`` so that all entries + survive (:issue:`957`). +- Fixed ``ContactSensor.compute_first_contact`` and ``compute_first_air`` + occasionally missing events when a contact began or ended right at the + last physics substep of a control step. ``current_contact_time`` / + ``current_air_time`` accumulate in float32 and can drift a few ULPs past + ``dt``, but the default ``abs_tol`` of ``1e-8`` sat at the noise floor + and rejected the comparison. Raised the default to ``1e-6``, which stays + well below typical control ``dt`` while comfortably covering float32 + accumulation noise (:issue:`933`). Contribution by @paLeziart. +- Fixed ``out_of_terrain_bounds`` using stale terrain dimensions. It read + ``TerrainGeneratorCfg.num_cols`` directly, which is ignored in curriculum + mode (the generator uses ``len(sub_terrains)`` columns instead), and it + did not account for ``border_width``. The termination now reads the + effective grid shape from ``terrain.terrain_origins`` and includes the + border in the footprint, so robots no longer reset while still on valid + terrain (or fail to reset after running off it) (:issue:`923`). +- ``ObservationManager`` now skips observation groups that end up with + zero active terms (e.g. all terms set to ``None``) with a log message, + instead of crashing later in ``torch.stack``/``torch.cat``. This lets + a shared runner config define groups that become empty under certain + runtime flags (e.g. model-specific terms all disabled for one variant). + The whole group can still be set to ``None`` to disable it explicitly. +- Fixed a runtime broadcast error in ``ContactSensor`` when combining + ``num_slots > 1`` with ``track_air_time=True`` and more than one primary. + Air-time tracking now reduces ``found`` across slots so that a primary is + considered in contact when any of its slots reports a match (:issue:`914`). +- Updated the ``create_new_task.ipynb`` Colab tutorial to import + ``XmlActuatorCfg`` instead of the removed ``XmlVelocityActuatorCfg``. + Added a regression test (``tests/test_notebooks.py``) that parses each + notebook cell and verifies that every ``from mjlab... import X`` + reference resolves, so future renames in the mjlab public API can't + silently rot the tutorials (:issue:`913`). +- Fixed ``ObservationManager`` silently sharing a single ``NoiseModelCfg`` + instance across observation groups that declared terms with the same + name. ``_group_obs_class_instances`` was keyed by term name alone, so + the last group processed in ``_prepare_terms`` overwrote earlier + groups' instances. Symptoms included the wrong noise config being + applied, shared per-episode state for ``NoiseModelWithAdditiveBias`` + (e.g. bias drawn from the wrong ``bias_noise_cfg``), and missed + ``reset()`` calls for overwritten instances. Instances are now keyed + by ``(group_name, term_name)`` so each group owns its own noise model. +- Fixed ``CurriculumManager.get_active_iterable_terms`` raising + ``TypeError`` when a term's state was a dict. The dict branch indexed + the output list by term name instead of appending to the local ``data`` + list. No in-tree caller currently invokes this method, so the bug was + latent. + +Version 1.3.0 (April 14, 2026) +------------------------------ + +Added +^^^^^ + +- Added ``ManagerBasedRlEnvCfg.auto_reset`` flag. When ``True`` (default), + ``step()`` continues to reset done environments in place and returns the + post-reset observation. When ``False``, ``step()`` skips the reset block + and returns the terminal observation directly; the caller must call + ``reset(env_ids=...)`` for done environments before the next ``step()`` + or a ``RuntimeError`` is raised. Enables access to the true terminal + state for algorithms that need it. Note that mjlab's bundled ``train.py`` + uses rsl_rl's ``OnPolicyRunner``, which does not drive manual resets, so + ``auto_reset=False`` is intended for custom training loops (:issue:`900`). +- Added ``ActuatorCfg.viscous_damping`` for passive velocity proportional + damping (``f = -b·v``), distinct from the PD derivative gain ``damping`` + used by position and velocity actuators. Maps to ```` for + JOINT transmission and ```` for TENDON transmission. + Defaults to ``None`` (preserves the XML value). +- Added :class:`~mjlab.managers.RecorderManager` for logging observations, + actions, or arbitrary environment data during rollouts. Implement a + :class:`~mjlab.managers.RecorderTerm` subclass and register it in the + ``recorders`` dict on ``ManagerBasedRlEnvCfg``. The manager provides + ``record_pre_reset``, ``record_post_reset``, and ``record_post_step`` + lifecycle hooks with no opinion on how data is stored. +- Added :func:`~mjlab.envs.mdp.curriculums.termination_curriculum` for + scheduling changes to termination term parameters during training, + matching the existing ``reward_curriculum`` pattern. Both now share a + single internal engine with init-time validation of stage ordering, + field existence, and param keys. +- Added ``reduce`` field to ``MetricsTermCfg``. Setting ``reduce="last"`` + reports the value from the final step of the episode rather than the + episode mean, which is useful for binary success metrics. +- Added :class:`~mjlab.envs.mdp.actions.RelativeJointPositionAction` for + joint position control relative to the current configuration. The target is + ``current_pos + action * scale``, so a zero action holds the current + configuration rather than commanding the default pose. +- Added :func:`~mjlab.envs.mdp.dr.pair_friction` for randomizing geom-pair + friction overrides (``pair_friction`` in ``mjModel``), with an + ``isotropic=True`` option that mirrors the symmetric tangent and roll + axes so single-axis randomization does not leave the paired axis stale. +- Added ``STAIRS_TERRAINS_CFG`` terrain preset for progressive stair + curriculum training and ``@terrain_preset`` decorator for composing + terrain configurations from reusable presets. +- Added cartpole balance and swingup tasks (``Mjlab-Cartpole-Balance`` and + ``Mjlab-Cartpole-Swingup``) with a :ref:`tutorial ` + that walks through building an environment from scratch. +- Added :ref:`motion imitation ` documentation with + preprocessing instructions. The README now links here instead of the + BeyondMimic repository, which produced incompatible NPZ files when used + with mjlab (:issue:`777`). +- Added ``margin``, ``gap``, and ``solmix`` fields to ``CollisionCfg`` + for per geom contact parameter configuration (:issue:`766`). +- NaN guard now captures mocap body poses (``mocap_pos``, ``mocap_quat``) + when the model has mocap bodies, enabling full state reconstruction in + the dump viewer for fixed-base entities. +- Implemented ``ActionTermCfg.clip`` for clamping processed actions after + scale and offset (:issue:`771`). +- Added ``qfrc_actuator`` and ``qfrc_external`` generalized force accessors + to ``EntityData``. ``qfrc_actuator`` gives actuator forces in joint space + (projected through the transmission). ``qfrc_external`` recovers the + generalized force from body external wrenches (``xfrc_applied``) + (:issue:`776`). +- Added ``RewardBarPanel`` to the Viser viewer, showing horizontal bars for + each reward term with a running mean over ~1 second (:issue:`800`). +- Added ``per_substep`` flag to ``MetricsTermCfg`` for evaluating metrics + once per physics substep inside the decimation loop. The per substep + values are averaged within each environment step, so episode averages + remain comparable to regular per step metrics. +- Added ``project-instinct/InstinctMJ`` to the research page's list of + projects built on mjlab. +- Added a Checkpoints tab to the Viser play viewer for hot-swapping + checkpoints without restarting. Works with local directories and W&B + runs (:issue:`751`). Contribution by @omarrayyann. +- Added ``"segmentation"`` camera data type for per-pixel geom ID output + alongside RGB and depth, and a multi-cube goal-conditioned lifting task + (``Mjlab-Multi-Cube-Seg-Yam``) that uses it (:issue:`862`). + Contribution by @pthangeda. + +Changed +^^^^^^^ + +- Renamed the ``list_envs`` console script to ``list-envs`` for consistency + with the other hyphenated entry points (``viz-nan``, ``export-scene``). + Invoke via ``uv run list-envs``. +- ``ActuatorCfg.armature`` and ``ActuatorCfg.frictionloss`` now default to + ``None`` instead of ``0.0``. ``None`` preserves the value defined in the + XML. Previously, builtin actuators would silently overwrite XML joint and + tendon properties with zero when these fields were not explicitly set. + To restore the old behavior, pass ``armature=0.0`` or ``frictionloss=0.0`` + explicitly. +- Actuator delay is now configured inline on any ``ActuatorCfg`` subclass + (e.g. ``BuiltinPositionActuatorCfg(..., delay_min_lag=2, delay_max_lag=5)``) + instead of wrapping with ``DelayedActuatorCfg``. ``DelayedActuator``, + ``DelayedActuatorCfg``, and ``DelayedBuiltinActuatorGroup`` are removed. +- Removed ``delay_target`` from ``ActuatorCfg``. Delay now always applies to + the actuator's ``command_field`` automatically. Multi-target delay + (``delay_target=("position", "velocity")``) is no longer supported. +- ``XmlPositionActuatorCfg``, ``XmlVelocityActuatorCfg``, ``XmlMotorActuatorCfg``, + and ``XmlMuscleActuatorCfg`` are replaced by a single ``XmlActuatorCfg`` that auto + detects the actuator type from XML. Pass ``command_field=...`` to override detection. +- Replaced the viser viewer internals with the ``mjviser`` package. Scene + creation, mesh conversion, and overlay rendering (contacts, forces, + inertia, tendons, joints, frames) are now provided by mjviser. The viewer + exposes a new Visualization tab for overlay controls and a Groups tab for + geom/site visibility. Debug visualization and warp tensor conversion remain + in mjlab's ``MjlabViserScene`` subclass (:issue:`839`). +- In curriculum terrain mode, each terrain type now gets exactly one column + (``num_cols`` is set to ``len(sub_terrains)``). The ``proportion`` field + now controls robot spawning distribution across columns rather than column + count. Random mode is unchanged (:issue:`811`). +- ``BoxSteppingStonesTerrainCfg`` stone size now decreases with difficulty, + interpolating from the large end of ``stone_size_range`` at difficulty 0 + to the small end at difficulty 1 (:issue:`785`). +- Removed deprecated ``TerrainImporter`` and ``TerrainImporterCfg`` aliases. + Use ``TerrainEntity`` and ``TerrainEntityCfg`` instead (:issue:`667`). +- ``Entity.clear_state()`` is deprecated. Use ``Entity.reset()`` instead. + ``clear_state`` only zeroed actuator targets without resetting actuator + internal state (e.g. delay buffers), which could cause stale commands + after teleporting the robot to a new pose. +- Removed ``EntityData.generalized_force``. The property was bugged (indexed + free joint DOFs instead of articulated DOFs) and the name was ambiguous. + Use ``qfrc_actuator`` or ``qfrc_external`` instead (:issue:`776`). +- ``get_wandb_checkpoint_path`` now filters checkpoints server-side via the + ``pattern`` parameter, avoiding unnecessary pagination and tolerance to + corrupted metadata (:issue:`898`). + +Fixed +^^^^^ + +- ``train`` and ``play`` now print a top-level usage message when invoked + with ``-h`` / ``--help`` and no task argument, pointing users at + ``list-envs`` and `` --help`` (:issue:`905`). +- Fixed ghost geom filtering in the Viser viewer. Ghost geoms were selected + by collision flags, so collision-disabled robot geoms appeared as ghosts. + The viewer now uses visual alpha to determine which geoms to render. +- Scene now warns when an attached entity or terrain spec has non-default + ``