Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions .github/workflows/sim-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Sim Check

# Produces a diagnostic render for each changed behavior that declares a
# registry-owned recipe. Shared runner changes exercise only a small golden
# set; full-catalog runs remain an explicit manual action.

on:
pull_request:
branches: [main, master]
paths:
- "registry/behaviors/**"
- "registry/schema/**"
- "simulation/**"
- ".github/workflows/sim-check.yml"
workflow_dispatch:
inputs:
behavior:
description: "Behavior id to check (empty = all descriptors)"
required: false
default: ""

permissions:
contents: read

env:
MUJOCO_GL: egl
PYOUT: sim-results

jobs:
detect:
name: Detect changed behaviors
runs-on: ubuntu-latest
outputs:
ids: ${{ steps.set.outputs.ids }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- id: set
run: |
requested="${{ github.event.inputs.behavior }}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "$requested" ]; then
ids=$(jq -cn --arg id "$requested" '[$id]')
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
ids=$(find registry/behaviors -maxdepth 1 -name '*.json' -printf '%f\n' \
| sed 's/\.json$//' | sort | jq -R -s -c 'split("\n") | map(select(length > 0))')
else
base="${{ github.event.pull_request.base.sha }}"
changed=$(git diff --name-only "$base" HEAD)
descriptor_ids=$(printf '%s\n' "$changed" \
| sed -n 's#registry/behaviors/\(.*\)\.json$#\1#p' \
| sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
shared=$(printf '%s\n' "$changed" \
| sed -n '\#^simulation/\|^registry/schema/\|^\.github/workflows/sim-check.yml$#p')
if [ -n "$shared" ]; then
golden='["alpha-walking","jump","max-height-jump","roulade"]'
ids=$(jq -cn --argjson changed "$descriptor_ids" --argjson golden "$golden" \
'$changed + $golden | unique')
else
ids="$descriptor_ids"
fi
fi
echo "ids=$ids" >> "$GITHUB_OUTPUT"
echo "changed behaviors: $ids"

simulate:
name: Sim ${{ matrix.id }}
needs: detect
if: needs.detect.outputs.ids != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
id: ${{ fromJson(needs.detect.outputs.ids) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install GL + ffmpeg
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq libegl1 libgl1 ffmpeg

- name: Install Python deps
run: pip install -r simulation/requirements.txt

- name: Cache pinned sim assets
uses: actions/cache@v4
with:
path: .simcache
key: sim-assets-${{ hashFiles('simulation/assets.lock.json') }}

- name: Test simulation runner
run: PYTHONPATH=simulation python -m unittest discover -s simulation/tests

- name: Run simulation check
run: |
python simulation/run_check.py \
--behavior "${{ matrix.id }}" \
--out "$PYOUT" \
--keep-media

- name: Upload sim report + render
if: always()
uses: actions/upload-artifact@v4
with:
name: sim-${{ matrix.id }}
path: |
${{ env.PYOUT }}/${{ matrix.id }}/report.json
${{ env.PYOUT }}/${{ matrix.id }}/loop.mp4
${{ env.PYOUT }}/${{ matrix.id }}/poster.png
if-no-files-found: warn
retention-days: 14

- name: Job summary
if: always()
run: |
{
echo "## Sim check: ${{ matrix.id }}"
echo "Diagnostic render only — this does not validate hardware behavior or reproduce arbitrary publisher environments."
echo
echo "Download \`sim-${{ matrix.id }}\` from the artifacts on [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})."
echo
if [ -f "$PYOUT/${{ matrix.id }}/report.json" ]; then
echo '```json'
cat "$PYOUT/${{ matrix.id }}/report.json"
echo '```'
else
echo "No report produced (run error)."
fi
} >> "$GITHUB_STEP_SUMMARY"
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ pnpm-debug.log*
vendor/
*.ses
session-*.md

# CI simulation
.simcache/
sim-results/
.research/

# Python
__pycache__/
*.pyc
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ uDuck Registry is a directory of Microduck behavior policies. A contribution is

For a local media path such as `/media/my-move/loop.mp4`, include the matching file at `public/media/my-move/loop.mp4` in the pull request.

### CI simulation check

Every pull request that adds or edits a descriptor is automatically run
through the registry's headless MuJoCo runner only when it declares an explicit
`simulation` recipe. `compatibility.robotd_slot` never selects the simulation
scenario. The workflow uploads `report.json`, `loop.mp4`, and `poster.png` for
human review; it does not publish them automatically.

The report distinguishes a completed render from its individual observations.
Requested checks use a fixed registry vocabulary, and their results are
measured by the runner—not authored in the descriptor. A render is not hardware
verification or proof that a publisher's training environment was reproduced.
The runner rejects recipes it cannot represent before downloading the policy;
it does not silently clamp command values.

If the policy requires custom environment code, objects, meshes, dependencies,
or a different observation/action contract, use `"runner": "external"` with
an honest reason and provide publisher-owned media instead. Do not give CI a
convenient but inaccurate command schedule just so the policy can be rendered.
Do not add per-policy executable code to this repository.

See [`simulation/README.md`](simulation/README.md) for recipe examples, start
states, supported scenarios, exact report semantics, and CI isolation rules.

## Descriptor shape

```json
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ The table below is generated from the descriptors in `registry/behaviors/`.

<!-- END GENERATED BEHAVIOR TABLE -->

## Simulation CI

Pull requests touching `registry/behaviors/` are automatically run through a
headless MuJoCo diagnostic (`Sim Check` workflow) when the descriptor declares
an explicit registry simulation recipe. The runner records exactly what it
observed and produces a standardized 512×512 review artifact; it does not claim
hardware validation or reproduce arbitrary publisher environments.
See [`simulation/README.md`](simulation/README.md) for the render standard,
scenario model, CI isolation rules, and unsupported cases.

## Machine-readable access

The generated catalog is available at:
Expand Down
Binary file added public/media/registry-sim/alpha-walking/loop.mp4
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions public/media/registry-sim/alpha-walking/report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"execution": "rendered",
"checks_status": "passed",
"checks": [
{
"check": "finite_outputs",
"passed": true,
"detail": "max |action| = 0.7221"
},
{
"check": "bounded_drift",
"passed": true,
"detail": "displacement 0.469 m"
},
{
"check": "no_fall",
"passed": true,
"detail": "min trunk height 0.1137 m"
},
{
"check": "ends_upright",
"passed": true,
"detail": "final tilt 3.92 deg"
},
{
"check": "velocity_tracking",
"passed": true,
"detail": "steady-state speed >= 30% of command (worst 41%), direction cos >= 0.8 (worst 0.98), mean |v_cmd - v_xy| = 0.189 m/s"
}
],
"observations": {
"duration_s": 6.0,
"control_steps": 300,
"obs_dim": 61,
"command_dim": 13,
"min_trunk_height_m": 0.1137,
"max_trunk_height_m": 0.122,
"final_trunk_height_m": 0.1214,
"max_tilt_deg": 4.53,
"final_tilt_deg": 3.92,
"path_length_m": 0.721,
"displacement_m": 0.469,
"max_abs_action": 0.7221,
"all_finite": true,
"initial_foot_contact": true,
"initial_bilateral_contact": true,
"airborne_observed": false,
"takeoff_after_support": false,
"touchdown_after_takeoff": false,
"mean_tracking_error_mps": 0.1892
},
"behavior": "alpha-walking",
"recipe": {
"runner": "microduck-standard-v1",
"scene": "flat-v1",
"start": {
"preset": "settled_standing"
},
"scenario": "velocity"
},
"duration_s": 6.0,
"policy": {
"url": "https://huggingface.co/spaces/pollen-robotics/microduck-simulator/resolve/main/app/public/policies/BEST_alpha_walking.onnx",
"sha256": "e36332d383997d51401897734cd3e79cf5038406feddb18b4d57ecfb141daa6c",
"baked_normalizer": true
},
"media": {
"loop_url": "/media/registry-sim/alpha-walking/loop.mp4",
"poster_url": "/media/registry-sim/alpha-walking/poster.png"
},
"generated_at": "2026-09-02T04:49:04.155415+00:00",
"runtime": {
"mjcf": "robot_allcollisions.xml (pollen-robotics/microduck-simulator, pinned)",
"timestep_s": 0.005,
"decimation": 4,
"control_hz": 50,
"renderer": "mujoco EGL offscreen"
}
}
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions public/media/registry-sim/ball-kick-left/report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"execution": "rendered",
"checks_status": "passed",
"checks": [
{
"check": "finite_outputs",
"passed": true,
"detail": "max |action| = 1.8749"
},
{
"check": "bounded_drift",
"passed": true,
"detail": "displacement 0.023 m"
},
{
"check": "no_fall",
"passed": true,
"detail": "min trunk height 0.1092 m"
},
{
"check": "ends_upright",
"passed": true,
"detail": "final tilt 0.9 deg"
}
],
"observations": {
"duration_s": 2.5,
"control_steps": 125,
"obs_dim": 61,
"command_dim": 13,
"min_trunk_height_m": 0.1092,
"max_trunk_height_m": 0.1186,
"final_trunk_height_m": 0.1143,
"max_tilt_deg": 5.52,
"final_tilt_deg": 0.9,
"path_length_m": 0.039,
"displacement_m": 0.023,
"max_abs_action": 1.8749,
"all_finite": true,
"initial_foot_contact": true,
"initial_bilateral_contact": true,
"airborne_observed": true,
"takeoff_after_support": true,
"touchdown_after_takeoff": true
},
"behavior": "ball-kick-left",
"recipe": {
"runner": "microduck-standard-v1",
"scene": "flat-v1",
"start": {
"preset": "settled_standing"
},
"scenario": "oneshot_zero"
},
"duration_s": 2.5,
"policy": {
"url": "https://huggingface.co/spaces/pollen-robotics/microduck-simulator/resolve/main/app/public/policies/ball_kick_left.onnx",
"sha256": "d6928284dccd3dd61e08bf2f760effa74309fbefd97b2b31afb2a60f526d196a",
"baked_normalizer": true
},
"media": {
"loop_url": "/media/registry-sim/ball-kick-left/loop.mp4",
"poster_url": "/media/registry-sim/ball-kick-left/poster.png"
},
"generated_at": "2026-09-02T04:49:06.900744+00:00",
"runtime": {
"mjcf": "robot_allcollisions.xml (pollen-robotics/microduck-simulator, pinned)",
"timestep_s": 0.005,
"decimation": 4,
"control_hz": 50,
"renderer": "mujoco EGL offscreen"
}
}
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading