diff --git a/docs/sphinx/source/en/2-user_guide/3-backends/0-index.md b/docs/sphinx/source/en/2-user_guide/3-backends/0-index.md index a599f5bda..efa766318 100644 --- a/docs/sphinx/source/en/2-user_guide/3-backends/0-index.md +++ b/docs/sphinx/source/en/2-user_guide/3-backends/0-index.md @@ -1,11 +1,11 @@ # Simulation Backends The tensor-only Manager runtime currently exposes `mujoco`, `mjwarp`, `genesis`, -`newton`, `motrix`, and the scoped Drake owner described below. User commands -select one with `--sim`, which routes to the matching task owner YAML; do not -switch a run by overriding `training.sim_backend` alone. +`newton`, `motrix`, `superdex`, and the scoped Drake owner described below. +User commands select one with `--sim`, which routes to the matching task owner +YAML; do not switch a run by overriding `training.sim_backend` alone. -The `isaacgym`, `isaacsim`, and `superdex` adapters remain temporarily shelved +The `isaacgym` and `isaacsim` adapters remain temporarily shelved by `unisim-core` during issue #1811. Their historical pages are retained for adapter context only and are not production support claims. The train/eval CLI rejects these names until new capability, parity, and support-matrix evidence is @@ -24,6 +24,9 @@ provided. - Motrix requires the `motrix` extra and provides a CPU-authoritative packed HOST_BRIDGE; the current canonical support scope is SAC `g1_walk_flat` and FlashSAC `g1_motion_tracking`. +- SuperDex requires the `superdex` extra on CPython 3.12/3.13 Linux x86_64 and + provides a CPU-authoritative packed HOST_BRIDGE; the current scope is the + configured Go2 and FR3 research owners. - Drake requires the locally built DrakeUni batch extension. Its scoped PPO `go2_joystick_flat` owner uses CPU physics and the packed HOST_BRIDGE lifecycle; floating-root and reset-randomization events remain disabled until @@ -38,6 +41,7 @@ provided. | Genesis | Linux x86_64 | Required: NVIDIA GPU and driver; only the `gs.gpu` channel is validated | | Newton | Linux | Required: NVIDIA CUDA; the selected-reset lane is device-resident | | Motrix | Linux / macOS / Windows | CPU-authoritative physics; Torch CUDA buffers are optional | +| SuperDex | Linux x86_64 (CPython 3.12/3.13) | CPU-authoritative physics; Torch CUDA buffers are optional | | Drake | Linux x86_64 / Apple Silicon macOS | CPU-authoritative physics; Torch CUDA buffers are optional | Backend device requirements are independent of the learner device: MuJoCo can @@ -57,7 +61,7 @@ the algorithm in `--algo`, not in `--task`. | Default path or broadest owner coverage | MuJoCo | | MuJoCo-only tools such as `scripts/play_viser.py` | MuJoCo | | Device-resident tensor owner | MJWarp, Genesis, or Newton, when the task support matrix marks the combination supported | -| CPU-authoritative packed HOST_BRIDGE | Motrix | +| CPU-authoritative packed HOST_BRIDGE | Motrix; SuperDex or scoped Drake for their configured research owners | The support matrix is generated from registry, owner YAML, and tests; use it as the current evidence source: {doc}`../../5-reference/5-support_matrix`. @@ -110,7 +114,7 @@ uv run python -c "import unisim; print(unisim.ADAPTER_SPECS)" ``` `unisim` has no dependency on UniLab, Hydra, or training components. The scoped -MuJoCo, MJWarp, Genesis, Newton, and Motrix adapters and the temporarily +MuJoCo, MJWarp, Genesis, Newton, Motrix, and SuperDex adapters and the temporarily shelved adapters use one public contract. Missing proprietary SDKs or GPU workers produce an explicit cold-path diagnostic; no backend silently falls back to another engine. @@ -122,4 +126,5 @@ back to another engine. 5-genesis 7-newton 2-motrix +8-superdex ``` diff --git a/docs/sphinx/source/en/2-user_guide/3-backends/8-superdex.md b/docs/sphinx/source/en/2-user_guide/3-backends/8-superdex.md index 3098e3ece..e82a9f4e2 100644 --- a/docs/sphinx/source/en/2-user_guide/3-backends/8-superdex.md +++ b/docs/sphinx/source/en/2-user_guide/3-backends/8-superdex.md @@ -1,13 +1,15 @@ # SuperDex Backend -> **SuperDex is temporarily shelved.** This adapter is outside the tensor-only Manager runtime during #1811 and is not a production support claim. The train/eval CLI rejects it until new capability, parity, and support-matrix evidence is provided. +> SuperDex participates in the tensor-only Manager runtime as a packed CPU +> `HOST_BRIDGE` backend. Its Go2 and FR3 owners remain **Configured** research +> profiles, not full-training or cross-platform production claims. SuperDex is an optional CPU physics adapter owned by `unisim.backend.superdex`. The initial UniLab owner is the fixed-base `FR3JointTarget` task: `src/unilab/conf/ppo/task/fr3_joint_target/superdex.yaml`. It uses seven torque -actions, 21 observation values, joint-state resets and the standard NumPy -manager runtime. Its support level is **Configured**; bounded rollout or short +actions, 21 observation values, selected tensor joint-state resets, and +`TorchEnv`. Its support level is **Configured**; bounded rollout or short training checks do not establish full-training performance or platform support. The implementation is tracked in [#1534](https://github.com/Motphys/UniLab/issues/1534) under [roadmap #1533](https://github.com/Motphys/UniLab/issues/1533). diff --git a/docs/sphinx/source/en/5-reference/5-support_matrix.md b/docs/sphinx/source/en/5-reference/5-support_matrix.md index 3f54e755d..9d4f71ff7 100644 --- a/docs/sphinx/source/en/5-reference/5-support_matrix.md +++ b/docs/sphinx/source/en/5-reference/5-support_matrix.md @@ -79,32 +79,34 @@ This table is derived from UniSim's SDK-free public static inventory. It describ | `drake` | Host bridge / in-process / host bridge | CPU / CUDA | Required only when the learner requests CUDA state/control buffers | Supported: CPU-authoritative physics with optional CUDA Torch buffers | CPU-authoritative host bridge only; no CUDA physics claim | CPU-authoritative host bridge only; no ROCm CUDA-only fallback | In-process; no external Python worker | Unsupported | Unsupported | Unsupported | Exact | | `mjwarp` | Device-resident / in-process / direct | CUDA | Required for the entire tensor lifecycle | Supported: Linux CUDA only | Unsupported; no CPU, MPS, or ROCm fallback | Unsupported; no CPU, MPS, or ROCm fallback | In-process; no external Python worker | Unsupported | Unsupported | Unsupported | Unsupported | | `newton` | Device-resident / in-process / direct | CUDA | Required for the entire tensor lifecycle | Supported: Linux CUDA only | Unsupported; no CPU, MPS, or ROCm fallback | Unsupported; no CPU, MPS, or ROCm fallback | In-process; no external Python worker | Unsupported | Unsupported | Unsupported | Unsupported | +| `superdex` | Host bridge / in-process / host bridge | CPU / CUDA | Required only when the learner requests CUDA state/control buffers | Supported: CPU-authoritative physics with optional CUDA Torch buffers | CPU-authoritative host bridge only; no CUDA physics claim | CPU-authoritative host bridge only; no ROCm CUDA-only fallback | In-process; no external Python worker | Unsupported | Unsupported | Unsupported | Exact | | `genesis` | Device-resident / in-process / direct | CUDA | Required for the entire tensor lifecycle | Supported: Linux CUDA only | Unsupported; no CPU, MPS, or ROCm fallback | Unsupported; no CPU, MPS, or ROCm fallback | In-process; no external Python worker | Unsupported | Unsupported | Unsupported | Unsupported | ### Entrypoint x Task Owner -| Entrypoint | Task owner | MuJoCo | Motrix | Drake | mjwarp | Newton | Genesis | -|------------|------------|---|---|---|---|---|---| -| PPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Configured | - | - | - | -| PPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | Configured | -| PPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | -| PPO (torch) | `x2_wall_flip_tracking` (X2 wall flip tracking) | Tested | - | - | - | - | - | -| PPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | -| PPO (torch) | `allegro_inhand_grasp` (allegro inhand grasp) | Tested | - | - | - | - | - | -| PPO (torch) | `g1_box_tracking` (g1 box tracking) | Tested | - | - | - | - | - | -| PPO (torch) | `stewart_balance` (stewart balance) | Tested | - | - | - | - | - | -| APPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | - | -| APPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Registered | Registered | Registered | -| APPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | -| APPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | -| SAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Tested | - | Tested | Tested | Tested | -| SAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Configured | Registered | Configured | -| SAC (torch) | `g1_wbt_obs` (g1 wbt obs) | Tested | - | - | - | - | - | -| FlashSAC (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | - | -| FlashSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Configured | Registered | Registered | -| FlashSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Tested | - | Configured | Configured | Configured | -| WarpSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | Registered | -| WarpSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Tested | Registered | Registered | +| Entrypoint | Task owner | MuJoCo | Motrix | Drake | mjwarp | Newton | SuperDex | Genesis | +|------------|------------|---|---|---|---|---|---|---| +| PPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Configured | - | - | Configured | - | +| PPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | - | Configured | +| PPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | - | +| PPO (torch) | `x2_wall_flip_tracking` (X2 wall flip tracking) | Tested | - | - | - | - | - | - | +| PPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | - | +| PPO (torch) | `allegro_inhand_grasp` (allegro inhand grasp) | Tested | - | - | - | - | - | - | +| PPO (torch) | `fr3_joint_target` (fr3 joint target) | - | - | - | - | - | Configured | - | +| PPO (torch) | `g1_box_tracking` (g1 box tracking) | Tested | - | - | - | - | - | - | +| PPO (torch) | `stewart_balance` (stewart balance) | Tested | - | - | - | - | - | - | +| APPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | Registered | - | +| APPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Registered | Registered | - | Registered | +| APPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | - | +| APPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | - | +| SAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Tested | - | Tested | Tested | - | Tested | +| SAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Configured | Registered | - | Configured | +| SAC (torch) | `g1_wbt_obs` (g1 wbt obs) | Tested | - | - | - | - | - | - | +| FlashSAC (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | Registered | - | +| FlashSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Configured | Registered | - | Registered | +| FlashSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Tested | - | Configured | Configured | - | Configured | +| WarpSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | - | Registered | +| WarpSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Tested | Registered | - | Registered | ### Source Index diff --git a/docs/sphinx/source/zh_CN/2-user_guide/3-backends/0-index.md b/docs/sphinx/source/zh_CN/2-user_guide/3-backends/0-index.md index c01361040..0d67e1000 100644 --- a/docs/sphinx/source/zh_CN/2-user_guide/3-backends/0-index.md +++ b/docs/sphinx/source/zh_CN/2-user_guide/3-backends/0-index.md @@ -1,11 +1,11 @@ # 仿真后端 当前 tensor-only Manager runtime 暴露 `mujoco`、`mjwarp`、`genesis`、 -`newton`、`motrix`,以及下文描述的 scoped Drake owner。用户命令通过 `--sim` -选择后端,并组合对应的 task owner YAML;不要单独覆盖 +`newton`、`motrix`、`superdex`,以及下文描述的 scoped Drake owner。用户命令 +通过 `--sim` 选择后端,并组合对应的 task owner YAML;不要单独覆盖 `training.sim_backend`。 -`isaacgym`、`isaacsim` 与 `superdex` 适配器在 issue #1811 期间由 +`isaacgym` 与 `isaacsim` 适配器在 issue #1811 期间由 `unisim-core` 暂时搁置。历史页面仅用于 adapter 背景,不构成生产支持声明。 在提供新的 capability、parity 与支持矩阵证据之前,train/eval CLI 会直接 拒绝这些后端。 @@ -21,6 +21,9 @@ 为 SAC `g1_walk_flat` 和 FlashSAC `g1_motion_tracking`。 - Motrix 需要 `motrix` extra,提供 CPU-authoritative packed HOST_BRIDGE;当前 canonical 支持范围为 SAC `g1_walk_flat` 和 FlashSAC `g1_motion_tracking`。 +- SuperDex 在 CPython 3.12/3.13 Linux x86_64 上需要 `superdex` extra,提供 + CPU-authoritative packed HOST_BRIDGE;当前范围为 Go2 与 FR3 configured + research owner。 - Drake 需要本地编译的 DrakeUni batch extension。其 scoped PPO `go2_joystick_flat` owner 使用 CPU physics 与 packed HOST_BRIDGE lifecycle;在 backend contract 补齐前,浮动根重置与 reset randomization @@ -35,6 +38,7 @@ | Genesis | Linux x86_64 | 要求:NVIDIA GPU 与 driver;仅验证 `gs.gpu` channel | | Newton | Linux | 要求:NVIDIA CUDA;selected-reset 通道为 device-resident | | Motrix | Linux / macOS / Windows | CPU-authoritative physics;Torch CUDA buffer 可选 | +| SuperDex | Linux x86_64(CPython 3.12/3.13) | CPU-authoritative physics;Torch CUDA buffer 可选 | | Drake | Linux x86_64 / Apple Silicon macOS | CPU-authoritative physics;Torch CUDA buffer 可选 | 后端设备要求与 learner 设备独立:MuJoCo 仍可以让 learner 使用 CUDA、ROCm、MPS @@ -52,7 +56,7 @@ UniLab 通过 task owner config 选择仿真后端。常规用法通过 `--task` | 默认路径或 owner 覆盖最广 | MuJoCo | | `scripts/play_viser.py` 等 MuJoCo 专属工具 | MuJoCo | | Device-resident tensor owner | MJWarp、Genesis 或 Newton,且支持矩阵标记该组合为 supported | -| CPU-authoritative packed HOST_BRIDGE | Motrix | +| CPU-authoritative packed HOST_BRIDGE | Motrix;SuperDex 或 scoped Drake 的 configured research owner | 支持矩阵由 registry、owner YAML 与测试生成;当前证据源见 {doc}`../../5-reference/5-support_matrix`。 @@ -104,7 +108,7 @@ uv run python -c "import unisim; print(unisim.ADAPTER_SPECS)" ``` `unisim` 不依赖 UniLab、Hydra 或训练组件。scoped -MuJoCo/MJWarp/Genesis/Newton/Motrix 适配器与暂时搁置的适配器使用同一个 +MuJoCo/MJWarp/Genesis/Newton/Motrix/SuperDex 适配器与暂时搁置的适配器使用同一个 public contract。缺失 proprietary SDK 或 GPU worker 时会产生明确的 cold-path diagnostic;不会静默切换到另一个引擎。 @@ -115,4 +119,5 @@ cold-path diagnostic;不会静默切换到另一个引擎。 5-genesis 7-newton 2-motrix +8-superdex ``` diff --git a/docs/sphinx/source/zh_CN/2-user_guide/3-backends/8-superdex.md b/docs/sphinx/source/zh_CN/2-user_guide/3-backends/8-superdex.md index 406a3c8ce..a40d57add 100644 --- a/docs/sphinx/source/zh_CN/2-user_guide/3-backends/8-superdex.md +++ b/docs/sphinx/source/zh_CN/2-user_guide/3-backends/8-superdex.md @@ -1,12 +1,14 @@ # SuperDex 后端 -> **SuperDex 暂时搁置。** 该适配器在 #1811 期间不属于 tensor-only Manager runtime,也不构成生产支持声明。在提供新的 capability、parity 与支持矩阵证据之前,train/eval CLI 会直接拒绝该后端。 +> SuperDex 已作为 packed CPU `HOST_BRIDGE` 后端加入 tensor-only Manager +> runtime。Go2 与 FR3 owner 仍是 **Configured** 研究配置,不构成完整训练或 +> 跨平台生产支持声明。 SuperDex 是由 `unisim.backend.superdex` 拥有的可选 CPU 物理后端。UniLab 首个 owner 为固定基 `FR3JointTarget`,配置位于 `src/unilab/conf/ppo/task/fr3_joint_target/superdex.yaml`。任务使用 7 维力矩动作、 -21 维观测、关节状态 reset 和标准 NumPy manager。当前支持等级为 **Configured**; +21 维观测、selected tensor 关节 reset 和 `TorchEnv`。当前支持等级为 **Configured**; 短 rollout 或少量训练迭代不能证明完整训练效果、性能或跨平台支持。 实施见 [#1534](https://github.com/Motphys/UniLab/issues/1534),所属 roadmap 为 [#1533](https://github.com/Motphys/UniLab/issues/1533)。 diff --git a/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md b/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md index c0a14212a..a3ec8004c 100644 --- a/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md +++ b/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md @@ -70,32 +70,34 @@ uv run scripts/generate_support_matrix.py --write | `drake` | Host bridge / in-process / host bridge | CPU / CUDA | Required only when the learner requests CUDA state/control buffers | Supported: CPU-authoritative physics with optional CUDA Torch buffers | CPU-authoritative host bridge only; no CUDA physics claim | CPU-authoritative host bridge only; no ROCm CUDA-only fallback | In-process; no external Python worker | 不支持 | 不支持 | 不支持 | 支持 | | `mjwarp` | Device-resident / in-process / direct | CUDA | Required for the entire tensor lifecycle | Supported: Linux CUDA only | Unsupported; no CPU, MPS, or ROCm fallback | Unsupported; no CPU, MPS, or ROCm fallback | In-process; no external Python worker | 不支持 | 不支持 | 不支持 | 不支持 | | `newton` | Device-resident / in-process / direct | CUDA | Required for the entire tensor lifecycle | Supported: Linux CUDA only | Unsupported; no CPU, MPS, or ROCm fallback | Unsupported; no CPU, MPS, or ROCm fallback | In-process; no external Python worker | 不支持 | 不支持 | 不支持 | 不支持 | +| `superdex` | Host bridge / in-process / host bridge | CPU / CUDA | Required only when the learner requests CUDA state/control buffers | Supported: CPU-authoritative physics with optional CUDA Torch buffers | CPU-authoritative host bridge only; no CUDA physics claim | CPU-authoritative host bridge only; no ROCm CUDA-only fallback | In-process; no external Python worker | 不支持 | 不支持 | 不支持 | 支持 | | `genesis` | Device-resident / in-process / direct | CUDA | Required for the entire tensor lifecycle | Supported: Linux CUDA only | Unsupported; no CPU, MPS, or ROCm fallback | Unsupported; no CPU, MPS, or ROCm fallback | In-process; no external Python worker | 不支持 | 不支持 | 不支持 | 不支持 | ### Entrypoint x Task Owner -| Entrypoint | Task owner | MuJoCo | Motrix | Drake | mjwarp | Newton | Genesis | -|------------|------------|---|---|---|---|---|---| -| PPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Configured | - | - | - | -| PPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | Configured | -| PPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | -| PPO (torch) | `x2_wall_flip_tracking` (X2 wall flip tracking) | Tested | - | - | - | - | - | -| PPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | -| PPO (torch) | `allegro_inhand_grasp` (allegro inhand grasp) | Tested | - | - | - | - | - | -| PPO (torch) | `g1_box_tracking` (g1 box tracking) | Tested | - | - | - | - | - | -| PPO (torch) | `stewart_balance` (stewart balance) | Tested | - | - | - | - | - | -| APPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | - | -| APPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Registered | Registered | Registered | -| APPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | -| APPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | -| SAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Tested | - | Tested | Tested | Tested | -| SAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Configured | Registered | Configured | -| SAC (torch) | `g1_wbt_obs` (g1 wbt obs) | Tested | - | - | - | - | - | -| FlashSAC (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | - | -| FlashSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Configured | Registered | Registered | -| FlashSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Tested | - | Configured | Configured | Configured | -| WarpSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | Registered | -| WarpSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Tested | Registered | Registered | +| Entrypoint | Task owner | MuJoCo | Motrix | Drake | mjwarp | Newton | SuperDex | Genesis | +|------------|------------|---|---|---|---|---|---|---| +| PPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Configured | - | - | Configured | - | +| PPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | - | Configured | +| PPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | - | +| PPO (torch) | `x2_wall_flip_tracking` (X2 wall flip tracking) | Tested | - | - | - | - | - | - | +| PPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | - | +| PPO (torch) | `allegro_inhand_grasp` (allegro inhand grasp) | Tested | - | - | - | - | - | - | +| PPO (torch) | `fr3_joint_target` (fr3 joint target) | - | - | - | - | - | Configured | - | +| PPO (torch) | `g1_box_tracking` (g1 box tracking) | Tested | - | - | - | - | - | - | +| PPO (torch) | `stewart_balance` (stewart balance) | Tested | - | - | - | - | - | - | +| APPO (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | Registered | - | +| APPO (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Registered | Registered | - | Registered | +| APPO (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | - | - | - | - | - | - | +| APPO (torch) | `allegro_inhand` (Allegro in-hand) | Tested | - | - | - | - | - | - | +| SAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Tested | - | Tested | Tested | - | Tested | +| SAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Configured | Registered | - | Configured | +| SAC (torch) | `g1_wbt_obs` (g1 wbt obs) | Tested | - | - | - | - | - | - | +| FlashSAC (torch) | `go2_joystick_flat` (Go2 joystick) | Tested | - | Registered | - | - | Registered | - | +| FlashSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Configured | Registered | - | Registered | +| FlashSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Tested | - | Configured | Configured | - | Configured | +| WarpSAC (torch) | `g1_walk_flat` (G1 walk flat) | Tested | Registered | - | Tested | Registered | - | Registered | +| WarpSAC (torch) | `g1_motion_tracking` (G1 motion tracking) | Tested | Registered | - | Tested | Registered | - | Registered | ### Source Index diff --git a/scripts/tools/support_matrix.py b/scripts/tools/support_matrix.py index fe108b91f..e9ba99874 100644 --- a/scripts/tools/support_matrix.py +++ b/scripts/tools/support_matrix.py @@ -23,7 +23,7 @@ BACKENDS: tuple[str, ...] = tuple( backend for backend in _ALL_BACKENDS - if backend in {"mujoco", "mjwarp", "genesis", "newton", "motrix", "drake"} + if backend in {"mujoco", "mjwarp", "genesis", "newton", "motrix", "superdex", "drake"} ) SHELVED_BACKENDS = frozenset(_ALL_BACKENDS) - frozenset(BACKENDS) diff --git a/src/unilab/base/registry.py b/src/unilab/base/registry.py index 4136577f2..22bed8c06 100644 --- a/src/unilab/base/registry.py +++ b/src/unilab/base/registry.py @@ -47,12 +47,12 @@ def __call__( "genesis", "newton", "motrix", + "superdex", "drake", ) _SHELVED_SIM_BACKENDS = ( "isaacgym", "isaacsim", - "superdex", ) _SUPPORTED_SIM_BACKENDS = _TENSOR_MANAGER_SUPPORTED_SIM_BACKENDS + _SHELVED_SIM_BACKENDS # Drake is an explicit owner-selected CPU host bridge during its scoped diff --git a/src/unilab/cli.py b/src/unilab/cli.py index 872bb4ff5..d70e0da7c 100644 --- a/src/unilab/cli.py +++ b/src/unilab/cli.py @@ -22,6 +22,7 @@ "genesis", "newton", "motrix", + "superdex", "drake", ) # Adapters retained by UniSim but outside the scoped tensor Manager runtime @@ -29,7 +30,6 @@ _SHELVED_SIMS = ( "isaacgym", "isaacsim", - "superdex", ) SUPPORTED_RENDER_MODES = ("auto", "interactive", "record", "viser", "none") OFFPOLICY_ALGOS = {"sac", "flashsac", "warpsac"} @@ -159,6 +159,16 @@ def _check_runtime_requirements(algo: str, sim: str) -> None: "`pip install unilab[motrix]` (or `uv sync --extra motrix` in a source " "checkout)." ) + if sim == "superdex": + from unisim.backend.superdex.dependencies import superdex_dependencies_available + + if not superdex_dependencies_available(): + raise SystemExit( + "sim=superdex requires Python 3.12 or 3.13 on Linux x86_64 and the " + "SuperDex Physics/Robotics runtime. Install it with " + "`uv sync --extra superdex` in a source checkout " + "(or `pip install unilab[superdex]`)." + ) if sim == "drake": if find_spec("drake_uni") is None: raise SystemExit( diff --git a/src/unilab/conf/ppo/task/fr3_joint_target/base.yaml b/src/unilab/conf/ppo/task/fr3_joint_target/base.yaml index 8a9e1e168..bbae44787 100644 --- a/src/unilab/conf/ppo/task/fr3_joint_target/base.yaml +++ b/src/unilab/conf/ppo/task/fr3_joint_target/base.yaml @@ -38,9 +38,6 @@ env: fr3_joint[1-4]: [-20.0, 20.0] fr3_joint[5-7]: [-5.0, 5.0] events: - reset_scene_to_default: - func: unilab.envs.mdp.reset_scene_to_default - mode: reset reset_joints: func: unilab.tasks.manipulation.fr3.joint_target.ResetJointOffsets mode: reset diff --git a/src/unilab/conf/ppo/task/fr3_joint_target/superdex.yaml b/src/unilab/conf/ppo/task/fr3_joint_target/superdex.yaml new file mode 100644 index 000000000..d251c0167 --- /dev/null +++ b/src/unilab/conf/ppo/task/fr3_joint_target/superdex.yaml @@ -0,0 +1,35 @@ +# @package _global_ +defaults: + - /task/fr3_joint_target/base + - _self_ + +training: + task_name: FR3JointTarget + sim_backend: superdex + device: cpu + play_render_mode: interactive + play_env_num: 1 + +algo: + num_envs: 2 + num_steps_per_env: 32 + max_iterations: 100 + obs_groups: + actor: [actor] + critic: [actor] + actor: + hidden_dims: [64, 64] + distribution_cfg: + init_std: 0.25 + critic: + hidden_dims: [64, 64] + algorithm: + num_mini_batches: 1 + +env: + superdex_num_workers: 0 + superdex_assets_root: null + superdex_effort_limits: [20.0, 20.0, 20.0, 20.0, 5.0, 5.0, 5.0] + +play_profile: + enabled: false diff --git a/src/unilab/conf/ppo/task/go2_joystick_flat/superdex.yaml b/src/unilab/conf/ppo/task/go2_joystick_flat/superdex.yaml new file mode 100644 index 000000000..6ecba7eba --- /dev/null +++ b/src/unilab/conf/ppo/task/go2_joystick_flat/superdex.yaml @@ -0,0 +1,55 @@ +# @package _global_ +# SuperDex training profile: inherit the exact MuJoCo policy/observation owner. +# SuperDex contact compliance and slide-only friction are not MuJoCo physics +# equivalents. This profile permits that conversion explicitly, not silently. +defaults: + - /task/go2_joystick_flat/mujoco + - _self_ + +training: + task_name: Go2JoystickFlat + sim_backend: superdex + no_play: false + play_render_mode: record + dp_collector_cpu_ids: null + +algo: + num_envs: 1024 + max_iterations: 400 + algorithm: + num_mini_batches: 1 + +env: + commands: + twist: + ranges: + lin_vel_x: [0.5, 0.5] + lin_vel_y: [0.0, 0.0] + ang_vel_z: [0.0, 0.0] + superdex_num_workers: 0 + superdex_allow_contact_approximation: true + events: + reset_scene_to_default: + func: unilab.envs.mdp.reset_scene_to_default_tensor + reset_root_state_uniform: + func: unilab.envs.mdp.reset_root_state_uniform_tensor + pd_gains: null + +reward: + tracking_lin_vel: + weight: 4.0 + tracking_ang_vel: + weight: 0.8 + contact: + weight: 0.05 + swing_feet_z: + weight: 1.5 + base_height: + weight: -50.0 + action_rate: + weight: -0.05 + similar_to_default: + weight: -0.05 + +play_profile: + enabled: true diff --git a/src/unilab/envs/manager_based_rl_env.py b/src/unilab/envs/manager_based_rl_env.py index 91d247194..a866b4a0d 100644 --- a/src/unilab/envs/manager_based_rl_env.py +++ b/src/unilab/envs/manager_based_rl_env.py @@ -445,6 +445,7 @@ def _compile_tensor_read_plan(self) -> None: specs: list[SceneTensorReadSpec] = [] specs.extend(self._action_tensor_read_specs()) specs.extend(self._observation_tensor_read_specs()) + specs.extend(self._task_tensor_state_read_specs()) specs.extend(self._manager_term_tensor_read_specs()) specs.extend(self._command_tensor_read_specs()) self.scene._tensor_read_plan = ( @@ -575,6 +576,52 @@ def _manager_term_tensor_read_specs(self) -> list[SceneTensorReadSpec]: specs.extend(self._manager_term_body_read_specs(manager_name, name, term)) return specs + def _task_tensor_state_read_specs(self) -> list[SceneTensorReadSpec]: + """Collect explicit task-owned state-only packed reads.""" + specs: list[SceneTensorReadSpec] = [] + seen: set[str] = set() + for group_name, terms in self.observation_manager.active_terms.items(): + for name in terms: + term_cfg = self.observation_manager.get_term_cfg(group_name, name) + entity_name = self._declared_tensor_state_entity( + term_cfg.func, f"observation/{group_name}/{name}" + ) + if entity_name is None: + continue + self._append_tensor_state_spec(specs, seen, entity_name) + for manager_name, manager in ( + ("reward", self.reward_manager), + ("termination", self.termination_manager), + ): + for name in manager.active_terms: + term = manager.get_term_cfg(name).func + entity_name = self._declared_tensor_state_entity(term, f"{manager_name}/{name}") + if entity_name is None: + continue + self._append_tensor_state_spec(specs, seen, entity_name) + return specs + + @staticmethod + def _declared_tensor_state_entity(term: Any, label: str) -> str | None: + entity_name = getattr(term, "tensor_state_entity", None) + if entity_name is None: + return None + if not isinstance(entity_name, str) or not entity_name: + raise TypeError( + f"Manager tensor state declaration for term '{label}' requires a " + "non-empty entity_name" + ) + return entity_name + + @staticmethod + def _append_tensor_state_spec( + specs: list[SceneTensorReadSpec], seen: set[str], entity_name: str + ) -> None: + if entity_name in seen: + return + seen.add(entity_name) + specs.append(SceneTensorReadSpec(entity=entity_name)) + @staticmethod def _manager_term_body_read_specs( manager_name: str, term_name: str, term: Any diff --git a/src/unilab/tasks/__init__.py b/src/unilab/tasks/__init__.py index c2658f9de..a43935767 100644 --- a/src/unilab/tasks/__init__.py +++ b/src/unilab/tasks/__init__.py @@ -9,6 +9,7 @@ "unilab.tasks.locomotion.go2", "unilab.tasks.locomotion.g1", "unilab.tasks.manipulation.allegro_inhand", + "unilab.tasks.manipulation.fr3", "unilab.tasks.manipulation.stewart", "unilab.tasks.motion_tracking.g1", "unilab.tasks.motion_tracking.x2", diff --git a/src/unilab/tasks/locomotion/go2/__init__.py b/src/unilab/tasks/locomotion/go2/__init__.py index 8a21a5c6d..cb9494197 100644 --- a/src/unilab/tasks/locomotion/go2/__init__.py +++ b/src/unilab/tasks/locomotion/go2/__init__.py @@ -5,6 +5,7 @@ registry.register_env_config("Go2JoystickFlat", ManagerBasedRlEnvCfg) registry.register_env("Go2JoystickFlat", make_manager_based_rl_env, sim_backend="mujoco") +registry.register_env("Go2JoystickFlat", make_manager_based_rl_env, sim_backend="superdex") registry.register_env("Go2JoystickFlat", make_manager_based_rl_env, sim_backend="drake") __all__: list[str] = [] diff --git a/src/unilab/tasks/manipulation/fr3/__init__.py b/src/unilab/tasks/manipulation/fr3/__init__.py new file mode 100644 index 000000000..a4be1bdf7 --- /dev/null +++ b/src/unilab/tasks/manipulation/fr3/__init__.py @@ -0,0 +1,7 @@ +"""Hydra-owned fixed-base FR3 joint-target task registration.""" + +from unilab.base import registry +from unilab.envs import ManagerBasedRlEnvCfg, make_manager_based_rl_env + +registry.register_env_config("FR3JointTarget", ManagerBasedRlEnvCfg) +registry.register_env("FR3JointTarget", make_manager_based_rl_env, sim_backend="superdex") diff --git a/src/unilab/tasks/manipulation/fr3/joint_target.py b/src/unilab/tasks/manipulation/fr3/joint_target.py new file mode 100644 index 000000000..f8aff0909 --- /dev/null +++ b/src/unilab/tasks/manipulation/fr3/joint_target.py @@ -0,0 +1,118 @@ +"""Joint-target terms using the public entity/reset contracts.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import numpy as np +import torch + +if TYPE_CHECKING: + from unilab.base.entity import Entity + from unilab.managers import ManagerTermBaseCfg + from unilab.managers._types import ManagerBasedRlEnv + + +class JointTargetObservation: + """Bind one ordered joint target on the manager construction path.""" + + def __init__(self, cfg: ManagerTermBaseCfg, env: ManagerBasedRlEnv) -> None: + self._entity = cast("Entity", env.scene[cfg.params["entity_name"]]) + self.tensor_state_entity = cast(str, cfg.params["entity_name"]) + self._target = torch.as_tensor( + list(cfg.params["target"]), dtype=torch.float32, device=env.device + ) + expected = (self._entity.num_joints,) + if self._target.shape != expected or not bool(torch.isfinite(self._target).all()): + raise ValueError(f"FR3 joint target must be finite with shape {expected}") + self._target = self._target.contiguous() + + def __call__( + self, env: ManagerBasedRlEnv, entity_name: str, target: list[float] + ) -> np.ndarray | torch.Tensor: + del entity_name, target + read_plan = getattr(env.scene, "_tensor_read_plan", None) + if read_plan is not None: + return read_plan.joint_tensor_view(self._entity).joint_pos - self._target + return self._entity.data.joint_pos - self._target.numpy() + + +class JointTargetReward(JointTargetObservation): + """Reward current joint accuracy with a configured squared-error scale.""" + + def __init__(self, cfg: ManagerTermBaseCfg, env: ManagerBasedRlEnv) -> None: + super().__init__(cfg, env) + std = cfg.params["std"] + if ( + isinstance(std, bool) + or not isinstance(std, (int, float)) + or not torch.isfinite(torch.tensor(float(std))) + or std <= 0 + ): + raise ValueError("FR3 joint target reward std must be finite and positive") + self._variance = float(std) ** 2 + + def __call__( + self, env: ManagerBasedRlEnv, entity_name: str, target: list[float], std: float = 0.5 + ) -> np.ndarray | torch.Tensor: + read_plan = getattr(env.scene, "_tensor_read_plan", None) + if read_plan is not None: + error: np.ndarray | torch.Tensor = ( + read_plan.joint_tensor_view(self._entity).joint_pos - self._target + ) + else: + error = ( + torch.as_tensor( + self._entity.data.joint_pos, dtype=torch.float32, device=self._target.device + ) + - self._target + ) + if isinstance(error, torch.Tensor): + return torch.exp(-torch.sum(torch.square(error), dim=-1) / self._variance) + return torch.exp(-torch.sum(torch.square(torch.as_tensor(error)), dim=-1) / self._variance) + + +class ResetJointOffsets: + """Stage selected joint resets without requiring a floating root.""" + + def __init__(self, cfg: ManagerTermBaseCfg, env: ManagerBasedRlEnv) -> None: + self._entity = cast("Entity", env.scene[cfg.params["entity_name"]]) + self._ranges = [] + self.tensor_state_entity = cast(str, cfg.params["entity_name"]) + self.uses_tensor_rows = True + for name in ("position_range", "velocity_range"): + values = np.asarray(cfg.params[name], dtype=np.float64) + if values.shape != (2,) or not np.isfinite(values).all() or values[0] > values[1]: + raise ValueError(f"FR3 reset {name} must be a finite ordered pair") + self._ranges.append((float(values[0]), float(values[1]))) + + def __call__( + self, + env: ManagerBasedRlEnv, + env_ids: torch.Tensor | None, + entity_name: str, + position_range: list[float], + velocity_range: list[float], + ) -> None: + if env_ids is None: + raise ValueError("FR3 reset requires explicit environment IDs") + positions = self._entity.data.default_joint_pos_torch(env.device).index_select(0, env_ids) + velocities = self._entity.data.default_joint_vel_torch(env.device).index_select(0, env_ids) + if env.torch_rng is None: + raise NotImplementedError("FR3 tensor reset requires env.torch_rng") + noise = env.torch_rng.generator + position_noise = ( + torch.rand(positions.shape, generator=noise, device=positions.device) + * (self._ranges[0][1] - self._ranges[0][0]) + + self._ranges[0][0] + ) + velocity_noise = ( + torch.rand(velocities.shape, generator=noise, device=velocities.device) + * (self._ranges[1][1] - self._ranges[1][0]) + + self._ranges[1][0] + ) + self._entity.write_joint_state_tensor_to_sim( + positions + position_noise, + velocities + velocity_noise, + env_ids=env_ids.to(dtype=torch.int64), + ) diff --git a/src/unilab/tasks/migration_matrix.py b/src/unilab/tasks/migration_matrix.py index 53555aeaa..2c27b2c72 100644 --- a/src/unilab/tasks/migration_matrix.py +++ b/src/unilab/tasks/migration_matrix.py @@ -28,6 +28,7 @@ class TaskMigrationRecord: { "AllegroInhandRotation", "AllegroInhandRotationGrasp", + "FR3JointTarget", "Go2JoystickFlat", "StewartBalance", } diff --git a/tests/base/test_registry.py b/tests/base/test_registry.py index 7ef4093ad..fc0c76732 100644 --- a/tests/base/test_registry.py +++ b/tests/base/test_registry.py @@ -258,7 +258,7 @@ def test_register_env_invalid_backend_raises(): registry_mod.register_env(_name, _TestEnvA, "not_a_backend") -@pytest.mark.parametrize("backend", ["isaacgym", "isaacsim", "superdex"]) +@pytest.mark.parametrize("backend", ["isaacgym", "isaacsim"]) def test_register_env_rejects_shelved_backend(backend): """Shelved adapters fail closed instead of registering a runtime path.""" _name = "_TestShelvedBackendEnv" @@ -278,6 +278,7 @@ def test_default_backend_order_is_tensor_manager_scope(): "genesis", "newton", "motrix", + "superdex", ) diff --git a/tests/envs/locomotion/go2/test_manager_based_cfg.py b/tests/envs/locomotion/go2/test_manager_based_cfg.py index 62b2c342b..1b765be18 100644 --- a/tests/envs/locomotion/go2/test_manager_based_cfg.py +++ b/tests/envs/locomotion/go2/test_manager_based_cfg.py @@ -228,7 +228,7 @@ def test_go2_flat_registry_is_manager_only() -> None: assert bare_cfg.rewards == {} assert registry.list_registered_envs()["Go2JoystickFlat"] == { "config_factory": "ManagerBasedRlEnvCfg", - "available_backends": ["mujoco", "drake"], + "available_backends": ["mujoco", "superdex", "drake"], } diff --git a/tests/envs/test_fr3_superdex.py b/tests/envs/test_fr3_superdex.py new file mode 100644 index 000000000..69105bb7a --- /dev/null +++ b/tests/envs/test_fr3_superdex.py @@ -0,0 +1,156 @@ +"""FR3 owner composition and optional native CPU rollout integration.""" + +from __future__ import annotations + +import multiprocessing as mp +from pathlib import Path +from typing import Any + +import numpy as np +import pytest +import torch +from hydra import compose, initialize_config_dir +from hydra.core.global_hydra import GlobalHydra + +from unilab import cli +from unilab.base import registry +from unilab.base.base import EnvCfg +from unilab.base.config_adapter import BackendAdapter +from unilab.base.config_materialization import apply_cfg_overrides +from unilab.base.env_factory import registry_env_factory +from unilab.envs import ManagerBasedRlEnvCfg + +ROOT = Path(__file__).resolve().parents[2] + + +def _owner() -> tuple[Any, dict[str, Any]]: + GlobalHydra.instance().clear() + with initialize_config_dir(config_dir=str(ROOT / "src/unilab/conf/ppo"), version_base="1.3"): + owner = compose("config", overrides=["task=fr3_joint_target/superdex"]) + return owner, BackendAdapter(owner, root_dir=ROOT).build_task_env_cfg_override() + + +def test_fr3_owner_has_torque_control_without_free_root_terms( + monkeypatch: pytest.MonkeyPatch, +) -> None: + owner, overrides = _owner() + cfg = registry.materialize_env_config("FR3JointTarget") + assert isinstance(cfg, ManagerBasedRlEnvCfg) + apply_cfg_overrides(cfg, overrides) + cfg.validate() + assert cfg.scene is not None + assert cfg.scene.entities["robot"].root_body_name == "fr3_link0" + assert list(cfg.events) == ["reset_joints"] + assert list(cfg.observations["policy"].terms) == ["target_error", "joint_vel", "actions"] + assert cfg.superdex_effort_limits == [20.0] * 4 + [5.0] * 3 + assert cfg.superdex_num_workers == 0 + assert owner.training.play_render_mode == "interactive" + assert owner.training.play_env_num == 1 and owner.training.device == "cpu" + assert "superdex" in cli.SUPPORTED_SIMS + monkeypatch.setattr(cli, "_check_runtime_requirements", lambda *_: None) + command = cli.build_command( + mode="train", algo="ppo", task="fr3_joint_target", sim="superdex", overrides=[] + ) + assert "task=fr3_joint_target/superdex" in command + + +@pytest.mark.parametrize( + "kwargs", + [ + {"superdex_num_workers": True}, + {"superdex_num_workers": -1}, + {"superdex_num_workers": 1.5}, + {"superdex_assets_root": ""}, + {"superdex_effort_limits": [0.0]}, + {"superdex_effort_limits": [float("nan")]}, + ], +) +def test_superdex_owner_options_reject_invalid_values(kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="superdex"): + EnvCfg(**kwargs).validate() + + +def _require_runtime() -> None: + pytest.importorskip("superdex.physics") + pytest.importorskip("superdex.robotics") + from unilab.assets.hub import resolve_superdex_robot_asset + + try: + resolve_superdex_robot_asset("bots/arms/fr3_v2/fr3_v2.superdex_bot") + except Exception as exc: # noqa: BLE001 - any resolution failure means skip + pytest.skip( + "native FR3 integration needs the FR3 assets: allow Hugging Face download " + f"or set SUPERDEX_ASSETS_PATH to a local checkout ({exc})" + ) + + +def test_fr3_native_rollout_and_selected_reset() -> None: + _require_runtime() + _, overrides = _owner() + env = registry.make( + "FR3JointTarget", sim_backend="superdex", num_envs=2, env_cfg_override=overrides + ) + try: + state = env.init_state() + assert env.obs_groups_spec == {"obs": 21} + assert env.action_space.shape == (7,) + action = torch.full((2, 7), 0.05, dtype=torch.float32) + for _ in range(32): + state = env.step(action) + assert torch.isfinite(state.obs["obs"]).all() + assert torch.isfinite(state.reward).all() + before = env.scene["robot"].data.joint_pos.copy() + counters = state.info["steps"].clone() + obs, _ = env.reset(torch.tensor([0], dtype=torch.int64)) + assert obs["obs"].shape == (1, 21) + np.testing.assert_array_equal(env.scene["robot"].data.joint_pos[1], before[1]) + assert state.info["steps"][1] == counters[1] + torch.testing.assert_close( + obs["obs"][0, :7], + torch.as_tensor(env.scene["robot"].data.joint_pos[0], dtype=torch.float32) + - torch.tensor([0.1, -0.7, 0, -2.2, 0, 1.5, 1.57], dtype=torch.float32), + atol=1e-6, + rtol=0, + ) + finally: + env.close() + with pytest.raises(RuntimeError, match="closed"): + env.step(torch.zeros((2, 7), dtype=torch.float32)) + + +def _spawn_rollout(overrides: dict[str, Any]) -> tuple[int, ...]: + factory = registry_env_factory("FR3JointTarget", "superdex") + env = factory(num_envs=1, env_cfg_override=overrides) + try: + obs, _ = env.reset(torch.tensor([0], dtype=torch.int64)) + env.step(torch.zeros((1, 7), dtype=torch.float32)) + return obs["obs"].shape + finally: + env.close() + + +def _spawn_parallel_rollout(overrides: dict[str, Any]) -> tuple[int, ...]: + factory = registry_env_factory("FR3JointTarget", "superdex") + env = factory(num_envs=2, env_cfg_override={**overrides, "superdex_num_workers": 2}) + try: + obs, _ = env.reset(torch.tensor([0, 1], dtype=torch.int64)) + env.step(torch.zeros((2, 7), dtype=torch.float32)) + return obs["obs"].shape + finally: + env.close() + + +def test_fr3_factory_survives_spawn() -> None: + _require_runtime() + _, overrides = _owner() + with mp.get_context("spawn").Pool(1) as pool: + result = pool.apply_async(_spawn_rollout, (overrides,)) + assert result.get(timeout=90) == (1, 21) + + +def test_fr3_parallel_backend_can_run_inside_spawn_collector() -> None: + _require_runtime() + _, overrides = _owner() + with mp.get_context("spawn").Pool(1) as pool: + result = pool.apply_async(_spawn_parallel_rollout, (overrides,)) + assert result.get(timeout=150) == (2, 21) diff --git a/tests/envs/test_go2_superdex.py b/tests/envs/test_go2_superdex.py new file mode 100644 index 000000000..6626365b9 --- /dev/null +++ b/tests/envs/test_go2_superdex.py @@ -0,0 +1,172 @@ +"""Go2's research SuperDex owner preserves policy I/O and reset semantics.""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import numpy as np +import pytest +import torch +from hydra import compose, initialize_config_dir +from hydra.core.global_hydra import GlobalHydra +from omegaconf import OmegaConf + +from unilab.base import registry +from unilab.base.config_adapter import BackendAdapter +from unilab.utils.sim2sim import DENYLIST, CrossBackendIncompatibleError, resolve_sim2sim_config + +ROOT = Path(__file__).resolve().parents[2] + + +def _owner(backend: str) -> tuple[Any, dict[str, Any]]: + GlobalHydra.instance().clear() + with initialize_config_dir(config_dir=str(ROOT / "src/unilab/conf/ppo"), version_base="1.3"): + owner = compose("config", overrides=[f"task=go2_joystick_flat/{backend}"]) + return owner, BackendAdapter(owner, root_dir=ROOT).build_task_env_cfg_override() + + +def test_go2_superdex_preserves_mujoco_policy_contract() -> None: + source, _ = _owner("mujoco") + target, _ = _owner("superdex") + for field in DENYLIST: + assert OmegaConf.select(source, field) == OmegaConf.select(target, field), field + assert target.env.events.pd_gains is None + assert target.env.events.reset_root_state_uniform is not None + assert target.env.superdex_allow_contact_approximation is True + assert target.env.superdex_num_workers == 0 + assert target.env.sim_dt == source.env.sim_dt + assert target.env.ctrl_dt == source.env.ctrl_dt + + +def test_go2_superdex_native_rollout_and_free_root_reset() -> None: + pytest.importorskip("superdex.physics") + if not (ROOT / "src/unilab/assets/robots/go2/assets/base_0.obj").is_file(): + pytest.skip("native Go2 rollout requires the registered Go2 robot assets") + _, override = _owner("superdex") + env = registry.make( + "Go2JoystickFlat", sim_backend="superdex", num_envs=2, env_cfg_override=override + ) + try: + state = env.init_state() + assert env.action_space.shape == (12,) + assert env.obs_groups_spec == {"obs": 49, "critic": 52} + action = torch.zeros((2, 12), dtype=torch.float32) + for _ in range(32): + state = env.step(action) + assert torch.isfinite(state.reward).all() + assert all(torch.isfinite(value).all() for value in state.obs.values()) + before = env.scene["robot"].data.root_link_pos_w.copy() + obs, _ = env.reset(torch.tensor([0], dtype=torch.int64)) + assert obs["obs"].shape == (1, 49) + np.testing.assert_array_equal(env.scene["robot"].data.root_link_pos_w[1], before[1]) + np.testing.assert_allclose( + np.linalg.norm(env.scene["robot"].data.root_link_quat_w, axis=-1), 1.0, atol=1e-5 + ) + finally: + env.close() + + +def test_go2_superdex_parallel_matches_serial_env_and_unsorted_reset() -> None: + pytest.importorskip("superdex.physics") + if not (ROOT / "src/unilab/assets/robots/go2/assets/base_0.obj").is_file(): + pytest.skip("native Go2 rollout requires the registered Go2 robot assets") + _, override = _owner("superdex") + envs = [] + try: + for workers in (1, 2): + envs.append( + registry.make( + "Go2JoystickFlat", + sim_backend="superdex", + num_envs=4, + env_cfg_override={**override, "seed": 7, "superdex_num_workers": workers}, + ) + ) + serial, parallel = envs + for env in envs: + env.init_state() + rng = np.random.default_rng(11) + for _ in range(8): + action = torch.as_tensor(rng.uniform(-0.1, 0.1, (4, 12)), dtype=torch.float32) + reference, result = (env.step(action) for env in envs) + for key in reference.obs: + np.testing.assert_allclose( + result.obs[key], reference.obs[key], atol=2e-5, rtol=2e-5 + ) + np.testing.assert_allclose(result.reward, reference.reward, atol=2e-5, rtol=2e-5) + before = parallel.scene["robot"].data.root_link_pos_w.copy() + # Non-monotonic IDs span both shards and must retain caller row order. + selected = torch.tensor([3, 0], dtype=torch.int64) + ref_obs, _ = serial.reset(selected) + out_obs, _ = parallel.reset(selected) + for key in ref_obs: + np.testing.assert_allclose(out_obs[key], ref_obs[key], atol=2e-5, rtol=2e-5) + np.testing.assert_array_equal( + parallel.scene["robot"].data.root_link_pos_w[[1, 2]], before[[1, 2]] + ) + finally: + for env in reversed(envs): + env.close() + + +def test_go2_superdex_executes_mujoco_checkpoint(tmp_path: Path) -> None: + checkpoint_value = os.environ.get("UNILAB_SUPERDEX_GO2_CHECKPOINT") + if not checkpoint_value: + pytest.skip("set UNILAB_SUPERDEX_GO2_CHECKPOINT to a MuJoCo Go2 PPO checkpoint") + pytest.importorskip("superdex.physics") + import torch + + from unilab.training.run import algo_config_dict + from unilab.visualization.interactive_playback import ( + RslRlPlaybackConfig, + create_rsl_rl_playback_session, + infer_checkpoint_actor_input_dim, + make_sim2sim_preflight, + ) + + checkpoint = Path(checkpoint_value).resolve(strict=True) + owner, overrides = _owner("superdex") + # Validate source policy-I/O before any environment is constructed. + resolve_sim2sim_config(str(checkpoint.parent), owner, algo_name="ppo", strict=True) + incompatible = OmegaConf.create(OmegaConf.to_container(owner, resolve=True)) + incompatible.env.actions.joint_pos.scale = 0.5 + with pytest.raises(CrossBackendIncompatibleError, match="env.actions"): + resolve_sim2sim_config(str(checkpoint.parent), incompatible, algo_name="ppo", strict=True) + + env = registry.make( + "Go2JoystickFlat", sim_backend="superdex", num_envs=2, env_cfg_override=overrides + ) + try: + session, _, loaded = create_rsl_rl_playback_session( + playback_cfg=RslRlPlaybackConfig( + task="Go2JoystickFlat", + load_run=str(checkpoint), + checkpoint=None, + action_mode="policy", + policy_obs_mode="flat", + algo_log_name="rsl_rl_ppo", + log_root=str(tmp_path), + num_envs=2, + ), + env_factory=lambda n: env, + algo_config=algo_config_dict(owner), + root_dir=ROOT, + device="cpu", + checkpoint_resolver=lambda *_: str(checkpoint), + checkpoint_input_dim_reader=infer_checkpoint_actor_input_dim, + entrypoint_log_root=lambda *args, **kwargs: tmp_path, + sim2sim_preflight=make_sim2sim_preflight(owner, algo_name="ppo"), + guard_algo_name="ppo", + ) + assert loaded == str(checkpoint) and session.policy is not None + session.reset() + with torch.inference_mode(): + for _ in range(64): + obs = session.step_once() + assert all(torch.isfinite(value).all() for value in obs.values()) + assert np.isfinite(env.state.reward).all() + assert session.step_count == 64 + finally: + env.close() diff --git a/tests/scripts/test_ci_scope.py b/tests/scripts/test_ci_scope.py index 83841577b..90c5dce2f 100644 --- a/tests/scripts/test_ci_scope.py +++ b/tests/scripts/test_ci_scope.py @@ -5,7 +5,7 @@ ROOT = Path(__file__).parents[2] WORKFLOWS = ROOT / ".github" / "workflows" -SHELVED_BACKENDS = ("isaacgym", "isaacsim", "superdex") +SHELVED_BACKENDS = ("isaacgym", "isaacsim") # Some optional packages and identifiers contain these words without routing a # UniLab backend (for example CUDA nccl/cusparselt). The production gate is the # explicit ``--extra `` install pattern. diff --git a/tests/scripts/test_support_matrix.py b/tests/scripts/test_support_matrix.py index 7c9812cd2..f85e82461 100644 --- a/tests/scripts/test_support_matrix.py +++ b/tests/scripts/test_support_matrix.py @@ -23,7 +23,7 @@ def test_shelved_adapters_have_no_support_cells() -> None: """During #1811, shelved adapters are not production support claims.""" - assert SHELVED_BACKENDS == frozenset({"isaacgym", "isaacsim", "superdex"}) + assert SHELVED_BACKENDS == frozenset({"isaacgym", "isaacsim"}) for row in build_support_rows(ROOT): assert set(row.cells) == set(BACKENDS) assert set(row.cells).isdisjoint(SHELVED_BACKENDS) @@ -325,7 +325,15 @@ def test_support_matrix_marks_allegro_appo_backends_as_tested(): def test_generated_support_matrix_exposes_only_tensor_manager_backends() -> None: from scripts.tools import support_matrix - assert support_matrix.BACKENDS == ("mujoco", "motrix", "drake", "mjwarp", "newton", "genesis") + assert support_matrix.BACKENDS == ( + "mujoco", + "motrix", + "drake", + "mjwarp", + "newton", + "superdex", + "genesis", + ) def test_drake_scoped_owner_stays_configured_until_training_validation() -> None: diff --git a/tests/tasks/test_go2_tensor_runtime.py b/tests/tasks/test_go2_tensor_runtime.py index 71b7306be..8d4097669 100644 --- a/tests/tasks/test_go2_tensor_runtime.py +++ b/tests/tasks/test_go2_tensor_runtime.py @@ -352,6 +352,7 @@ def test_go2_runtime_does_not_add_backend_registrations() -> None: registry.ensure_registries() assert registry.list_registered_envs()["Go2JoystickFlat"]["available_backends"] == [ "mujoco", + "superdex", "drake", ] diff --git a/tests/tasks/test_package_boundary.py b/tests/tasks/test_package_boundary.py index 62cc094f0..a304b472a 100644 --- a/tests/tasks/test_package_boundary.py +++ b/tests/tasks/test_package_boundary.py @@ -16,6 +16,7 @@ "unilab.tasks.locomotion.go2", "unilab.tasks.locomotion.g1", "unilab.tasks.manipulation.allegro_inhand", + "unilab.tasks.manipulation.fr3", "unilab.tasks.manipulation.stewart", "unilab.tasks.motion_tracking.g1", "unilab.tasks.motion_tracking.x2", diff --git a/tests/test_cli.py b/tests/test_cli.py index 6dd624724..73ca29756 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -163,6 +163,89 @@ def test_eval_mujoco_interactive_honors_profile_and_render_override( ] +def _make_superdex_eval_checkout(root: Path) -> None: + (root / "scripts").mkdir(parents=True) + (root / "scripts" / "train_rsl_rl.py").write_text("", encoding="utf-8") + owner_dir = root / "conf" / "ppo" / "task" / "go2_joystick_flat" + owner_dir.mkdir(parents=True) + (owner_dir / "superdex.yaml").write_text( + "training:\n sim_backend: superdex\n", encoding="utf-8" + ) + + +def _pretend_superdex_runtime_is_available(monkeypatch: pytest.MonkeyPatch) -> None: + from unisim.backend.superdex import dependencies + + monkeypatch.setattr(dependencies, "superdex_dependencies_available", lambda: True) + + +def test_eval_superdex_interactive_forces_single_env_on_train_play_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _make_superdex_eval_checkout(tmp_path) + _pretend_superdex_runtime_is_available(monkeypatch) + + command = cli.build_command( + mode="eval", + algo="ppo", + task="go2_joystick_flat", + sim="superdex", + overrides=[], + load_run="-1", + render_mode="interactive", + root=tmp_path, + ) + + # SuperDex interactive eval keeps the train-script play path (the native + # Polyscope viewer renders it) but collapses to the single scene the + # viewer draws; the owner layer switches the env to the serial executor. + assert command[1] == str(tmp_path / "scripts" / "train_rsl_rl.py") + assert "training.play_render_mode=interactive" in command + assert "training.play_env_num=1" in command + + +def test_eval_superdex_interactive_preserves_explicit_play_env_num( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _make_superdex_eval_checkout(tmp_path) + _pretend_superdex_runtime_is_available(monkeypatch) + + command = cli.build_command( + mode="eval", + algo="ppo", + task="go2_joystick_flat", + sim="superdex", + overrides=["training.play_env_num=4"], + load_run="-1", + render_mode="interactive", + root=tmp_path, + ) + + assert "training.play_env_num=4" in command + assert "training.play_env_num=1" not in command + + +def test_eval_superdex_record_does_not_force_play_env_num( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _make_superdex_eval_checkout(tmp_path) + _pretend_superdex_runtime_is_available(monkeypatch) + + command = cli.build_command( + mode="eval", + algo="ppo", + task="go2_joystick_flat", + sim="superdex", + overrides=[], + load_run="-1", + render_mode="record", + root=tmp_path, + ) + + assert "training.play_render_mode=record" in command + assert not any(o.startswith("training.play_env_num=") for o in command) + + def test_eval_mujoco_interactive_preserves_explicit_action_mode( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_cli_runtime_requirements.py b/tests/test_cli_runtime_requirements.py index dcd56f83e..ea4c69933 100644 --- a/tests/test_cli_runtime_requirements.py +++ b/tests/test_cli_runtime_requirements.py @@ -73,4 +73,12 @@ def unavailable() -> tuple[bool, ImportError | None]: def test_cli_backend_choices_are_tensor_manager_scope() -> None: - assert cli.SUPPORTED_SIMS == ("mujoco", "mjwarp", "genesis", "newton", "motrix", "drake") + assert cli.SUPPORTED_SIMS == ( + "mujoco", + "mjwarp", + "genesis", + "newton", + "motrix", + "superdex", + "drake", + ) diff --git a/tests/training/test_training_helpers.py b/tests/training/test_training_helpers.py index c8c5bf25e..67f62c0ae 100644 --- a/tests/training/test_training_helpers.py +++ b/tests/training/test_training_helpers.py @@ -327,6 +327,19 @@ def test_backend_adapter_env_cfg_override_for_sac_g1_walk_flat(): assert cfg.algo.max_iterations == 5000 +def test_backend_adapter_injects_superdex_serial_mode_only_for_interactive_play(): + interactive = _ppo_cfg( + ["task=go2_joystick_flat/superdex", "training.play_render_mode=interactive"] + ) + adapter = BackendAdapter(interactive, root_dir=_ROOT_DIR, algo_name="ppo") + assert "superdex_execution_mode" not in adapter.build_task_env_cfg_override() + assert adapter.build_play_env_cfg_override()["superdex_execution_mode"] == "serial" + + record = _ppo_cfg(["task=go2_joystick_flat/superdex", "training.play_render_mode=record"]) + record_override = BackendAdapter(record, root_dir=_ROOT_DIR, algo_name="ppo") + assert "superdex_execution_mode" not in record_override.build_play_env_cfg_override() + + def test_backend_adapter_keeps_motion_manager_scene_during_play(): cfg = _ppo_cfg(["task=g1_motion_tracking/mujoco", "training.play_only=true"]) captured: dict[str, object] = {}