feat(tracking): v0.1.2 双手轨迹回放与 GUI 预览

支持 L20 右手模型身份与严格浮动控制覆盖层,新增完整轨迹受限回放、时间拉伸及可选 GUI 显示。

验证:88 项 CPU/USD 回归、Ruff、格式与独立暂存审查通过。历史 80 倍降速回放完成 2x32000 步;整合后 GUI E2E 和完整 pre-commit 未执行,相关边界见 L20_TRACKING.md。

右手 USD、示教数据、媒体和日志未纳入提交;资产存储及许可仍待确认。保留原控制与安全阈值。
This commit is contained in:
2026-09-14 13:57:57 +08:00
parent 057f4c2cf6
commit 8e7ab5fc76
20 changed files with 2218 additions and 62 deletions
+26
View File
@@ -6,6 +6,7 @@
**当前用途:浮动腕部与全部手指参考关节的轨迹回放,后续用于学习策略。**
本文第1–9节保持左手交付约定;右手接入差异见第10节,不得直接使用左手哈希、限位或拇指联动。
本文是可独立分享的数据交付说明,与当前仓库校验器一致。它不是通用 HDF5 标准,也不是完整 DexSchema。
请先交付 **1 条短样例**,确认坐标、尺度、关节映射后再批量导出。
@@ -215,3 +216,28 @@ export PYTHONPATH="$PWD/source/dex_workbench${PYTHONPATH:+:$PYTHONPATH}"
不是 HDF5 格式要求。合法的静止或局部手指轨迹不应为了满足诊断而添加伪造动作。
仿真实现与已有验证结果另见 [`L20_TRACKING.md`](L20_TRACKING.md)。本文只约定数据交付,不要求数据方运行 Isaac Sim。
## 10. 右手扩展:显式身份,不是左手镜像
本次校验器在 `l20_tracking_v1` 中接受 `hand_side="right"`;其余shape、dtype、坐标和有效帧语义不变。
旧版本校验器仅接受left,读取右数据必须升级本次代码。一个文件不能混合左右手episode。
右手必须使用 [`右手 tracking_manifest.json`](assets/robots/dex_hand/linkerhand_g20_right/tracking_manifest.json)
该清单明确 `hand_side=right`,绑定已检查的右URDF身份。旧无side的左清单仍可读,右清单不得省略side。
左右文件对另一侧清单均拒绝;即使只篡改数据hash/root以匹配另一侧,仍会被side校验拒绝。
- `embodiment="L20"`、`root_link="hand_base_link"` 不变;**hand_side改为right**。
- 右原始转换USD依赖包 `asset_sha256`
`d38dc5e4d473119b237b86bdd36087788f726abd9f486b1678f4e719d5513098`。
不是右prepared覆盖层哈希,不得复制到不同模型数据。
- 右当前也是21个状态关节;逐项名称与第5节列表一致,但必须仍按**右清单**排序核对,不能假设所有手型如此。
- 右拇指 `thumb_ip=1.03*thumb_mcp`,不是左手1.02;四指 `dip=.89*pip`offset均0。
- 右 `thumb_cmc_pitch/roll/mcp` 上限分别 `.83/1.39/1.25 rad`,四指pip为`1.75 rad`。
其他值及浮点精确值按右清单 `lower_rad/upper_rad`;不使用文档舍入值放宽限位。
- 右URDF `thumb_cmc_yaw`轴为负Z,USD通过局部关节旋转表达。零位/方向必须按右模型重定向,
不能直接把左轨迹换hand_side/哈希当作右轨迹,也不能简单给全部角度取负。
- 数据样例仍应来自同版右模型。右手初次接入仅验证合成HDF5;后续一个独立绑定的专家参考
已完成80倍慢放回放,见 `L20_TRACKING.md` 第5节;不证明原速或任意专家数据可回放。
给右手数据同事发送本文时,附上同版**右清单**与右模型坐标/映射说明。模型来源、转换设置、
本地使用及发布限制见 [`右手资产说明`](assets/robots/dex_hand/linkerhand_g20_right/README.md)。
+101 -1
View File
@@ -1,5 +1,11 @@
# L20 左手轨迹跟踪:数据交付与准备入口
本页保留左手验证历史。新增右手入口、实际转换/回放证据与发布限制见
[`右手资产说明`](assets/robots/dex_hand/linkerhand_g20_right/README.md);左右共用校验器与控制入口,显式绑定hand_side。
HDF5右手差异见独立契约第10节(右thumb联动1.03,不能套用左1.02)。
本轮准备门禁新增 `NewtonArticulationRootAPI` 注册上下文;**旧prepared覆盖层需重新生成**。
左手源资产未改,本轮额外左small两轮480步回归PASS,指标与历史baseline一致。
## 本次可用范围与验证状态
提供 **CPU 数据接口、资产清单、浮动控制覆盖层和实验性动力学跟踪入口**,不是完成的 RL 环境。
@@ -254,7 +260,8 @@ PYTHONPATH=source/dex_workbench timeout 300s ~/isaacsim/python.sh \
### 控制与验收边界
- 单环境、seed42、240Hz;默认两次各480步,单次允许1001200步,外部 timeout 300秒。
- 单环境、seed42、240Hz;默认两次各480步,普通模式单次允许1001200步,外部 timeout 300秒。
显式完整文件模式的有限步数/预算例外见第5节;不会自动扩大默认范围。
只创建手,无物体、桌面、任务注册或训练。不逐步 teleport;仅 reset 写 root/joint 初值并清零速度、
wrench 与目标缓冲、轨迹时间,之后只发 master q_target 和 root COM wrench。
- 世界系 PD 跟踪 link 原点;输出作用在 root COM,补上 `(p_link-p_rootCOM)×F_PD`
@@ -346,3 +353,96 @@ HDF5用例 RMS 依次为 `0.000646705 m`、`0.006378398 rad`、`0.007675343 rad`
仍有TGS外力迭代、16/21 actuator5个被动关节)、protobuf重复注册、visualizer/MaterialX与usdrt警告;
未关闭日志或放宽检查。更大范围、负向/多轴腕部运动、接触、真实专家文件、多环境、训练与硬件均未验证。
完整 pre-commit 仍因未安装工具而 NOT_RUN;本次独立审查仍由父会话执行。
## 5. 显式扩范围与完整专家参考回放
默认工作半径仍为起点周围 **0.1m**,普通模式步数上限仍为 **1200**(默认480)。
`--workspace-radius 0.8` 仅覆盖工作半径,不改变增益、力/力矩、参考速度、实测安全界限或误差阈值。
`--full-episode` 必须同时提供 HDF5,允许每轮最多 **32000** 步,并要求所选 episode 的完整末时刻
`steps/240` 一致;拒绝截尾、超时长外推或离网格的完整回放请求。始终两轮、单环境、seed42。
CPU `stretch-time` 仅将原始时间戳乘以有限且>=1的系数;保留全部样本、位姿/关节几何路径、有效掩码、
关节顺序、已应用的坐标变换和来源类别。在说明中追加系数与输入文件 SHA256,不将专家标成合成数据。
输入仍须严格匹配清单;**没有通用强制替换哈希的选项**。源包身份算法不同的交付必须先独立核对
原始 URDF 全部 link/joint、仿真依赖和交付说明,并保留原文件及绑定依据,再生成仅本地使用的派生文件。
```bash
# BOUND 是已经独立审查并严格绑定右手清单的本地派生文件,不是未经核验的原始交付。
# EVIDENCE 是仓库外持久目录;输出必须尚不存在。
MANIFEST=assets/robots/dex_hand/linkerhand_g20_right/tracking_manifest.json
~/isaacsim/python.sh -m dex_workbench_tracking.cli stretch-time "$BOUND" \
--manifest "$MANIFEST" --factor 80 --output "$EVIDENCE/slow80.hdf5"
~/isaacsim/python.sh -m dex_workbench_tracking.cli validate "$EVIDENCE/slow80.hdf5" --manifest "$MANIFEST"
# 下面是需单独授权600秒资源预算、且确认无其他Kit/GPU任务时的完整实验命令;不是自动重试授权。
timeout --kill-after=15s 600s ~/isaacsim/python.sh scripts/tracking/track_l20.py \
assets/robots/dex_hand/linkerhand_g20_right/tracking.usda --manifest "$MANIFEST" \
--hdf5 "$EVIDENCE/slow80.hdf5" --steps 32000 --full-episode --workspace-radius 0.8 \
--execute-experimental --headless
```
本次指定51帧专家参考原长1.666667秒,统一80倍慢放后完整133.333333秒/轮;不再次应用
`world_from_source`,不裁剪、重居中或改变关节轨迹。入口只创建手,没有地面/物体,世界Z为负不需要增添场景。
成功必须同时具备退出0、明确PASS、两轮完整32000步、全部原有动态/重置/位姿与速度重复性门禁;
失败输出记录阶段、已执行步数及可用的最后误差/速度。超时或缺少最终PASS不是成功,不为通过而继续调参。
慢放结果仅适用于该派生参考,不证明专家原速、接触任务、训练质量或Sim2Real有效。
### 恢复后的单次授权完整回放:PASS2026-09-14
此前中断运行只恢复出启动记录,没有结束/退出/PASS证据,结果保持未知;未将其算作通过。
用户另行允许的一次600秒预算运行完成上述命令:384秒退出0,明确PASS,完整两轮各32000步。
Isaac Sim `6.0.1-rc.7`、PhysX `110.1.13`、RTX5080,所有原有运行时与重复性断言通过。
两轮各自的 max/RMS 汇总相同(不宣称跨运行GPU确定性):
| 误差 | 最大值 | RMS |
| --- | ---: | ---: |
| 腕位置 | 0.004234530 m | 0.001592088 m |
| 腕姿态 | 0.013369173 rad | 0.006896696 rad |
| 各帧最大关节误差 | 0.011143208 rad | 0.007029774 rad |
| 各帧最大mimic残差 | 0.000231490 rad | 0.000100007 rad |
- 原文件SHA256 `bd9321096803fbb64f590b4fdeb9056edc11ac446a7e16458c509beedc8b9552`
本地80倍派生文件SHA256 `8f584c7409698394429737d509924013b8b90700117f8af2fb22465223dc1815`
- 原始22个link/21个joint逐元素核对、右USD依赖包、51个原样本及Data/左右资产文件字节保留通过。
准备层包SHA `3e2071a60496b23bed252f4e2fc6e1d4540eecc8098578506419b5eb594f7da0`
- 持久制品目录标识 `expert-replay-recovery-20260914/recovered-20260914T033624Z`
`expert.log``expert-metrics.json``expert-result.txt``exact-command.txt`、绑定脚本/原始诊断/哈希清单。
完整存储路径见本次交付报告;未提交数据/日志。原始日志SHA256
`c6557269ffe3583ed113b3a1dcf3be6f4f3791bcff36a67798fbb4dcf1f48446`
- 新鲜81项CPU回归无跳过、Ruff/格式/diff检查通过。额外的原时间网格与慢放网格数值相等审计
**NOT MET**8个关节插值值差1个float32 ULP,另2个近零消差值差约2.3e-18/2.6e-18rad。
保留失败证据,不为其加入索引特例或放宽运行门禁;51原始样本仍逐值完全相同。
- 启动前后无Kit/compute进程;没有再次启动、调增益或改轨迹。仍有TGS外力迭代、16/21 actuator
5个被动关节)、visualizer/MaterialX/usdrt警告。未做原速、多环境、任务/训练或硬件验收。
- 上述恢复回放阶段未暂存、提交、推送或升级版本;后续v0.1.2本地提交准备不代表完整发布门禁通过。
## 6. 可选GUI展示(v0.1.2
同一入口新增 `--gui`,复用生产轨迹采样、控制、reset、两轮重复性及所有身份/安全门禁。
默认仍headless、不渲染;`--gui` 与显式 `--headless``--viz none` 冲突时启动前报错。
GUI使用Kit visualizer,物理步长仍为1/240秒,仅每8步渲染(另在reset后初始渲染一次),不改时间戳或慢放系数。
不要同时启动第二个Kit/GPU会话;只有旧会话退出且另获600秒预算授权后才运行:
```bash
export PYTHONPATH="$PWD/source/dex_workbench${PYTHONPATH:+:$PYTHONPATH}"
# SLOW80_HDF5 由调用方提供:已独立绑定同版右清单、按第5节80倍慢放的文件;不是自动绑定原始交付。
RIGHT=assets/robots/dex_hand/linkerhand_g20_right
timeout --kill-after=15s 600s ~/isaacsim/python.sh scripts/tracking/track_l20.py \
"$RIGHT/tracking.usda" --manifest "$RIGHT/tracking_manifest.json" \
--hdf5 "$SLOW80_HDF5" --steps 32000 --full-episode --workspace-radius 0.8 \
--execute-experimental --gui
```
显示设置独立于物理,位于 `dex_workbench_tracking/preview.py`
- 白色Dome强度700key/fill分别800/300、角度12/20度、XYZ旋转(-35,25,25)/(50,-40,-120)度。
- 全episode腕路径为青色(0.1,0.8,1.0)线性曲线,恒定宽度0.002m;不改参考点。
- 固定相机target为完整路径包围盒中心,eye偏移(1.15,-1.35,0.95)m;不逐帧跟随或重居中轨迹。
- 面向相机的10m方形暗背景位于target背后2m,仅自己的非反射emissive材质(0.055,0.065,0.08)。
无RigidBody/Collision API,不修改手的任何材质,不增加任务物体/桌面/碰撞或改变控制。
用户已确认仓库外展示克隆中的明亮手掌/手指与暗背景可见。
本次将其纯展示代码整合为可复用选项并加CPU/USD不变量检查;**整合后的CLI GUI E2E为NOT_RUN**
没有为提交准备再次启动Kit/GPU,也未打断用户现有GUI。克隆截图不能替代最终源码运行验收。
生成的右USD、原始URDF/标定/网格、专家HDF5、视频/日志均不随代码提交;仅右README和两个JSON元数据纳入。
干净克隆仍需经授权取得同哈希右资产并重建prepared层;右资产测试/回放在缺依赖时BLOCKED,不计skip为PASS。
完整pre-commit因工具缺失NOT_RUN;本地版本提交不是存储、许可、硬件或完整Release就绪声明。
@@ -0,0 +1,114 @@
# L20 右手:转换资产与受限跟踪验证
## 状态与入口
用户提供的 `linker-bot/Linkerbot_URDF/L20/RIGHT/linkerhand_g20_right.urdf` 已使用指定
`IsaacLab/scripts/tools/convert_urdf.py` 实际转换。保留源文件与生成命名:URDF 内部 robot 名为
`linkerhand_L25NS_right.urdf`,导入器生成入口为
[`linkerhand_g20_right/linkerhand_g20_right.usda`](linkerhand_g20_right/linkerhand_g20_right.usda)。
用户明确该 RIGHT 目录为本次 L20 右手输入;不由上述名称推断驱动类型或电机数。
- 原始转换结果保持不变,依赖包完整、引用相对路径,可整体移动;不是左手 USD 镜像副本。
- [`tracking.usda`](tracking.usda) 是单独生成的**实验控制覆盖层**,需搭配跟踪控制器,不能直接 Play 当作悬浮手。
- [`tracking_manifest.json`](tracking_manifest.json) 记录原始转换资产身份、关节、限位、质量/惯量、联动与依赖。
`dynamic_replay_ready=false` 保留;实验入口检查实际覆盖层和运行时响应,不靠 ready 标记授权。
- [`source_provenance.json`](source_provenance.json) 记录原始 URDF/JSON/STL 的相对路径、字节数、SHA256、轴向与转换设置。
原始文件保留在用户外部源目录,不复制18MiB STL,也未读取 JSON 作为控制协议。
- **发布 BLOCKED**`payloads/geometries.usd`**4,329,903 bytes**,超过普通 Git 单文件2000KB门槛。
生成USD仅获准放入本地工作区,不纳入v0.1.2提交(仅收录本说明及两个JSON清单);后续须确认 Gitea LFS 或获批的版本化外部存储,不能直接提交普通 Git。
现有`.gitignore`全局忽略`*.usd`/`*.usda`,且`.gitattributes`已有全局LFS规则;本次均未修改。
资产目前是本地忽略文件,不会随普通代码提交自动分发;已有LFS属性不等于本轮确认远端存储/发布许可。
源资产再分发许可尚未确认;用户授权本地仿真不等于授予再分发许可。
## 转换及核验
实际转换设置预先确定:`--headless --fix-base --joint-stiffness 3 --joint-damping 0.1 --joint-target-type position`
不 merge joints,输出参数为**目录**。这些是未经硬件标定的仿真设置;不是从源硬件推断的增益。
转换输出先置于临时目录,核验后仅复制生成的 USD 目录;带个人源路径的 `config.yaml``.asset_hash` 和日志留在临时目录。
复现示例(环境变量由调用方指定,不写入 USD 引用):
```bash
# LINKERBOT_URDF 指向同版本源包;OUTPUT 必须是新的输出目录,避免覆盖已验证资产。
SOURCE_RIGHT="$LINKERBOT_URDF/L20/RIGHT"
timeout 300s ~/isaacsim/python.sh ~/IsaacLab/scripts/tools/convert_urdf.py \
"$SOURCE_RIGHT/linkerhand_g20_right.urdf" "$OUTPUT" --headless --fix-base \
--joint-stiffness 3 --joint-damping 0.1 --joint-target-type position
```
实际原始 URDF SHA256`aa198155651eb7e815f1c27bc8bd9c902e969e81c1165aed756779db378e715d`
转换源依赖包 SHA256`d38dc5e4d473119b237b86bdd36087788f726abd9f486b1678f4e719d5513098`
控制覆盖层完整包 SHA256`3e2071a60496b23bed252f4e2fc6e1d4540eecc8098578506419b5eb594f7da0`
导入器文档字符串带临时生成路径,不是外部资源引用;所有实际解析依赖均在本目录内。
实际检查结果:22个刚体、21个 revolute 状态关节、16个模型独立目标、5组被动联动。
名字排序与当前左手相同,但**几何/惯量/轴方向/限位不宣称相同**:
- 右拇指 `thumb_ip=1.03*thumb_mcp`(左手1.02);其他四指 `dip=.89*pip`,全部 offset=0。
-`thumb_cmc_pitch/roll/mcp` 上限 `.83/1.39/1.25 rad`,四指 pip上限 `1.75 rad`;其余精确值见清单。
- 原URDF `thumb_cmc_yaw` 轴为负ZUSD规范轴Z配合 `localRot1` 后为负Z。
全部21条变换后的轴与源URDF核对通过;不能只看USD的轴字母判断正负。
-`root_joint` 是叶 fixed joint,body0为非刚体且世界变换为单位阵的 default Xformbody1为根link。
ArticulationRoot 在 `Geometry` 容器,不是左手旧固定关节位置。
- 覆盖层只禁用/停用该叶锚点、迁移根API及去掉五个 follower angular drives。
右源有 `NewtonArticulationRootAPI`;安装 schema 明确继承 `PhysicsArticulationRootAPI`,因此一起迁移到根link
不能留在旧Geometry制造第二个有效根。原本存在的 Physics/Physx/Newton 根API迁移,保留旧/新位置已解析属性,
包括 `newton:selfCollisionEnabled=false`。左手没有的Newton根API不添加。
- 门禁要求精确静态端点、单位世界变换、无时间采样/重置绕过、精确根容器;其他prim状态、API、属性、关系和变换保持。
注册schema回归证明迁移后只有一个**有效**Physics根。Newton后端没有运行;仅验证 PhysX。
## 真实运行证据(2026-09-11
Isaac Sim6.0.1、PhysX110.1.13、RTX5080seed42、单环境、240Hz,无物体/桌面/训练。
完整预算4次 Kit 启动,每次外部timeout300s:转换、右small、右HDF5、左small回归;均退出0。
回放三次各有明确 PASS JSON、两轮完整步数、reset/位置及速度重复性、有限值/限位/被动非零运动检查。
三次均报告浮动根、21个活动状态关节、1个有效根、旧锚点inactive;没有重复/嵌套根或CreateJoint警告。
| 用例 | 每轮步数 × 轮数 | 最大腕位置误差 m | 最大腕姿态误差 rad | 最大关节误差 rad | 最大联动残差 rad |
| --- | --- | --- | --- | --- | --- |
| 右small2mmX、.01rad yaw、2s | 480×2 | .000366063 | .002292019 | .009957471 | .000525197 |
| 右range_4s HDF510mmX、.1rad yaw、4s | 960×2 | .001207785 | .010241941 | .018221961 | .000575949 |
| 左small回归 | 480×2 | .000492342 | .003755143 | .008628675 | .000221643 |
右range RMS同序:`.000761782 / .007442808 / .013643790 / .000266136`
关节/联动列先取每帧各关节最大绝对误差再汇总。两轮汇总相同,但不保证跨运行GPU位级确定性。
左回归与之前左small指标完全相同;左右为不同模型,不能把表中数值解释为同一控制性能 benchmark。
原有控制参数与阈值不变:腕P/D100/10、姿态P/D.2/.02、wrench20N/1Nmmaster驱动3/.1effort.2Nm、velocity.5rad/s。
合成轨迹使用实际右手限位与1.03联动,源参考有限且全有效;所有关节有零位及正向空间,无需负向替代/裁剪。
每步仍只发受限wrench及独立目标,不逐步teleport;只有reset写状态。
实际运行命令(仓库根目录):
```bash
export PYTHONPATH="$PWD/source/dex_workbench${PYTHONPATH:+:$PYTHONPATH}"
RIGHT=assets/robots/dex_hand/linkerhand_g20_right
timeout 300s ~/isaacsim/python.sh scripts/tracking/track_l20.py "$RIGHT/tracking.usda" \
--manifest "$RIGHT/tracking_manifest.json" --execute-experimental --headless --steps 480
~/isaacsim/python.sh -m dex_workbench_tracking.cli synthetic --manifest "$RIGHT/tracking_manifest.json" \
--profile range_4s --output "$EVIDENCE/range.hdf5"
timeout 300s ~/isaacsim/python.sh scripts/tracking/track_l20.py "$RIGHT/tracking.usda" \
--manifest "$RIGHT/tracking_manifest.json" --execute-experimental --headless --steps 960 \
--hdf5 "$EVIDENCE/range.hdf5" --episode demo_000000
```
`EVIDENCE` 由调用方指定为仓库外持久目录。生成HDF5拒绝覆盖;重跑请选择新路径。
仅在用户Kit会话已释放、预算另获授权后执行。
上述9月11日历史运行使用临时证据目录 `l20-right``{convert,small,hdf5,left}.log``*-result.txt`;源/轴核验、CPU回归、插件准备、
日志解析及前后GPU检查在同目录。每次运行前后未发现残留Kit;并不证明所有资源完全恢复。
75项CPU/USD测试通过,含右资产重定位、左右身份错配(即使伪造hash/root)、端点/容器篡改、
注册schema下Newton根继承与时间采样检查。完整pre-commit工具缺失,不能宣称全量发布门禁通过。
## 兼容性与剩余边界
- HDF5仍为`l20_tracking_v1`,新增显式`hand_side=right`,且必须配套右清单。旧无side左清单兼容;
右清单不得省略side或借用左URDF身份。旧版本读取器只认left,读右数据须升级本次代码。
- prepared schema context新增Newton根schema**此前生成的覆盖层需要重新生成**,不能编辑ready字段绕过。
生成时重复传入 `--schema-plugin-path`,分别指向安装的PhysX和Newton `plugInfo.json`,见根目录L20文档。
- 原始USD/URDF、左侧资产、Cartpole、任务ID及用户Data目录均未修改;v0.1.2仅版本化相关代码与文本元数据。
干净克隆缺少本地右USD依赖,右资产CPU测试/回放仍BLOCKED,不将缺资产跳过当成通过。
- 仍有TGS外力迭代、16/21 actuator(有意被动)、protobuf重复注册、visualizer/MaterialX/usdrt警告,未压制或调参消除。
- 上述9月11日证据仅两个简单合成轨迹。9月14日另一次80倍慢放专家参考的完整回放及GUI展示边界
见根目录 [`L20_TRACKING.md`](../../../../L20_TRACKING.md) 第5–6节;不证明专家原速、接触/自碰撞任务、
多环境、学习策略、Newton后端或硬件。增益待校准,无SysID/Sim2Real有效性声明。
@@ -0,0 +1,246 @@
{
"source_origin": "user-provided linker-bot/Linkerbot_URDF/L20/RIGHT",
"source_robot_name": "linkerhand_L25NS_right.urdf",
"license_status": "NOT_CONFIRMED; user-authorized local simulation, no redistribution grant inferred",
"source_files": [
{
"path": "g20_right_G20.json",
"bytes": 111584,
"sha256": "314865d5bc359996bb182942fefc597f989ebfa6c9b3c97a9086acf38b0712ee"
},
{
"path": "linkerhand_g20_right.urdf",
"bytes": 28687,
"sha256": "aa198155651eb7e815f1c27bc8bd9c902e969e81c1165aed756779db378e715d"
},
{
"path": "meshes/hand_base_link.STL",
"bytes": 3433184,
"sha256": "79e36d4c7e9282c79601976d41144557d1928810202589e31bed335f3d308cd1"
},
{
"path": "meshes/index_distal.STL",
"bytes": 984884,
"sha256": "324aa4b852e0c469bfc0c0bb9b19e0ccffc7dcb401d6a678481340588465fa26"
},
{
"path": "meshes/index_metacarpals.STL",
"bytes": 151784,
"sha256": "106ad58d52420c3216ccb87865f55062d4b4b558078dda95210a085a9bbe277c"
},
{
"path": "meshes/index_middle.STL",
"bytes": 805584,
"sha256": "27f9ccd91c5bdb1f6a1c42aeee4ab79e7e0aaf1e124a36999c75715b70d0aee6"
},
{
"path": "meshes/index_proximal.STL",
"bytes": 883184,
"sha256": "d4f85e69dd94d56bdc4646c945ca8a237c652fd5dffba86c7b9ce1d8067ae8e1"
},
{
"path": "meshes/middle_distal.STL",
"bytes": 984884,
"sha256": "688be3d9dd7fda49a8f9dbba824fedef5cf427c94e2e9e1ea96834eafbf6945a"
},
{
"path": "meshes/middle_metacarpals.STL",
"bytes": 151784,
"sha256": "65bc65257a01e0ccd284dfdf3b2a66f2a865bf80faeb4da746cd1fd05114fe40"
},
{
"path": "meshes/middle_middle.STL",
"bytes": 805584,
"sha256": "600b8e7486abc8a2707a5ecc66d4b5bea1dc0895c7173e911ba994f5cac30c10"
},
{
"path": "meshes/middle_proximal.STL",
"bytes": 883184,
"sha256": "edd3af02319cd4a3b4e07a6f5d33d501887d5ab78625489254f289785b6f57e1"
},
{
"path": "meshes/pinky_distal.STL",
"bytes": 984884,
"sha256": "3b22871d948ef80447f93866af3643ffa87cb22c1c172750fca9357f3aaa46de"
},
{
"path": "meshes/pinky_metacarpals.STL",
"bytes": 151784,
"sha256": "b63d10a0e18af0169c75b85eb740c1f25123f6c4c5be0c5368afc65e8c73771f"
},
{
"path": "meshes/pinky_middle.STL",
"bytes": 805584,
"sha256": "c6696bfcd2dbef8f248f86a6f45a90d5ba7b40e11cdb13a86b96347240a7b751"
},
{
"path": "meshes/pinky_proximal.STL",
"bytes": 883184,
"sha256": "311ce029c4972530d3b234307f4ea6ca1789577c2786d56f969a5dfac2ef6a57"
},
{
"path": "meshes/ring_distal.STL",
"bytes": 984884,
"sha256": "fca7bf31fa3f377110d31f7623e27f8c7845faedbdca7c09f98c94a28867bd69"
},
{
"path": "meshes/ring_metacarpals.STL",
"bytes": 151784,
"sha256": "11797279afee7f856e472755d3a96593baea659d2ec0014edb02006ae48b5d71"
},
{
"path": "meshes/ring_middle.STL",
"bytes": 805584,
"sha256": "2b3a0815e8daf95e76c9f54f84c13709a3951aa44ec6bb863facf60ba32c3c16"
},
{
"path": "meshes/ring_proximal.STL",
"bytes": 883184,
"sha256": "168df91e5a5643a899556f96ca3f3b7fe8a3349436be42dfb5632c0d524f9a47"
},
{
"path": "meshes/thumb_distal.STL",
"bytes": 1108484,
"sha256": "026dd2fac43c4461c5a90561ef29039f4a08802c0e47e5c422ecac2f34babe1e"
},
{
"path": "meshes/thumb_metacarpals.STL",
"bytes": 1158784,
"sha256": "0a82998ece3c784bba7861b850d00ed483d93434f94f72c6298fea0917e328ca"
},
{
"path": "meshes/thumb_metacarpals_base1.STL",
"bytes": 852984,
"sha256": "8416e5f826f12c4597754fbe3052983635345a76e3d98a4cdbc38ed363a9da78"
},
{
"path": "meshes/thumb_metacarpals_base2.STL",
"bytes": 227884,
"sha256": "a5679a28a612ab6aa62a4f7fa1c67be42f807bba7c2ca00ba062ca2e4ef0070d"
},
{
"path": "meshes/thumb_proximal.STL",
"bytes": 716984,
"sha256": "4cd7d6a283d2e5c3f6dfbd88aa1e18f9f481375d1e6b47f2c373a285184b123d"
}
],
"urdf_axes_xyz": {
"thumb_cmc_roll": [
1.0,
0.0,
0.0
],
"thumb_cmc_yaw": [
0.0,
0.0,
-1.0
],
"thumb_cmc_pitch": [
0.0,
1.0,
0.0
],
"thumb_mcp": [
0.0,
1.0,
0.0
],
"thumb_ip": [
0.0,
1.0,
0.0
],
"index_mcp_roll": [
1.0,
0.0,
0.0
],
"index_mcp_pitch": [
0.0,
1.0,
0.0
],
"index_pip": [
0.0,
1.0,
0.0
],
"index_dip": [
0.0,
1.0,
0.0
],
"middle_mcp_roll": [
1.0,
0.0,
0.0
],
"middle_mcp_pitch": [
0.0,
1.0,
0.0
],
"middle_pip": [
0.0,
1.0,
0.0
],
"middle_dip": [
0.0,
1.0,
0.0
],
"ring_mcp_roll": [
1.0,
0.0,
0.0
],
"ring_mcp_pitch": [
0.0,
1.0,
0.0
],
"ring_pip": [
0.0,
1.0,
0.0
],
"ring_dip": [
0.0,
1.0,
0.0
],
"pinky_mcp_roll": [
1.0,
0.0,
0.0
],
"pinky_mcp_pitch": [
0.0,
1.0,
0.0
],
"pinky_pip": [
0.0,
1.0,
0.0
],
"pinky_dip": [
0.0,
1.0,
0.0
]
},
"converter": "IsaacLab/scripts/tools/convert_urdf.py",
"converter_sha256": "55882895652f83b19b05b6fb38fc441ac60900ef6a1261d1dbabeabcbc189e65",
"converter_options": {
"headless": true,
"fix_base": true,
"merge_joints": false,
"joint_stiffness": 3.0,
"joint_damping": 0.1,
"joint_target_type": "position"
},
"generated_geometry_bytes": 4329903,
"publication_status": "BLOCKED: >2000KB geometry requires confirmed LFS or approved versioned external storage and redistribution licensing; generated USDs not staged or bundled"
}
@@ -0,0 +1,861 @@
{
"hand_side": "right",
"manifest_version": "l20_asset_manifest_v1",
"asset_sha256": "d38dc5e4d473119b237b86bdd36087788f726abd9f486b1678f4e719d5513098",
"entry_file": "linkerhand_g20_right.usda",
"dependencies": [
{
"path": "linkerhand_g20_right.usda",
"sha256": "dc8e7e4883b761df19853bf53381b5749146cbe597d1289fae1545664c4e70f4"
},
{
"path": "payloads/Physics/mujoco.usda",
"sha256": "4384c714631d20a731a92a99270e47ce9f355429594168db81509afe4e1d0ca5"
},
{
"path": "payloads/Physics/physics.usda",
"sha256": "c18dea5558b0161bb46b3e724271714d3ec1cc4af830f96335dcc27956ce0071"
},
{
"path": "payloads/Physics/physx.usda",
"sha256": "c6a2ffc8bf93985a3798677d4b7efd11cbed55b430859bdce861d2f5d7ca9024"
},
{
"path": "payloads/base.usda",
"sha256": "b8f90dc5bf297c15a0b28bca946aabd1561e96e502512431c8f269732c4adc06"
},
{
"path": "payloads/geometries.usd",
"sha256": "625aa0357691d74c0a420d76a6d177c1fed4fe76e6c22b237080d05285c45d1c"
},
{
"path": "payloads/instances.usda",
"sha256": "e1edfd4599e0dc2736fccf7b5fab8d9eb83e28a2603410f0dbb106a312c2c62f"
},
{
"path": "payloads/materials.usda",
"sha256": "63fb65168c347ebf2fc5c3c702cefe43f5c0352a3a611b762963535f921ae5ea"
},
{
"path": "payloads/robot.usda",
"sha256": "2e305dc2e5e6cc9e76f6dcc859e7e62baf28c6c4b07bb0b385bfcc875b3784c9"
}
],
"composition_errors": [],
"unresolved_dependencies": [],
"default_prim": "/tn__linkerhand_L25NS_righturdf_td0",
"root_link": "hand_base_link",
"root_body_path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link",
"units": {
"length": "m",
"mass": "kg",
"up_axis": "Z"
},
"physics_variant": "physx",
"articulation_roots": [
"/tn__linkerhand_L25NS_righturdf_td0/Geometry"
],
"world_fixed_joints": [
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/root_joint",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link"
}
],
"bodies": [
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.0023553327191621065,
-0.00047695066314190626,
0.08402746915817261
],
"physics:diagonalInertia": [
0.00011830108996946365,
0.00016542924277018756,
0.000269879907136783
],
"physics:mass": 0.1300438940525055,
"physics:principalAxes": "(0.70355177, -0.03574017, 0.7086691, 0.03905978)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
0.012130068615078926,
-0.0036862639244645834,
0.00025301380082964897
],
"physics:diagonalInertia": [
2.0386005417094566e-06,
2.4792493604763877e-06,
4.060510491399327e-06
],
"physics:mass": 0.017312832176685333,
"physics:principalAxes": "(0.6933548, -0.09771051, 0.69679224, 0.15553884)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
0.0027480488643050194,
0.007686008233577013,
0.003620089730247855
],
"physics:diagonalInertia": [
4.896461405223818e-07,
1.3818194020132069e-06,
1.932495933942846e-06
],
"physics:mass": 0.012929310090839863,
"physics:principalAxes": "(-0.19246794, 0.5912315, 0.61269253, 0.48786193)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
0.006804674863815308,
-0.0003489459923002869,
0.032902996987104416
],
"physics:diagonalInertia": [
3.568792408259469e-06,
1.1496833394630812e-05,
1.379148852720391e-05
],
"physics:mass": 0.031144972890615463,
"physics:principalAxes": "(0.5002009, 0.49657822, 0.50224656, 0.5009566)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals/thumb_proximal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.0026874335017055273,
0.000568290997762233,
0.011850360780954361
],
"physics:diagonalInertia": [
2.2336946869927488e-07,
4.898279257758986e-07,
6.232427836039278e-07
],
"physics:mass": 0.005490158684551716,
"physics:principalAxes": "(0.44667035, 0.4435007, 0.5443676, 0.5544877)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals/thumb_proximal/thumb_distal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.0006504820776171982,
2.4130056772264652e-05,
0.011194284074008465
],
"physics:diagonalInertia": [
1.259255100194423e-07,
3.968557109601534e-07,
4.377191942239733e-07
],
"physics:mass": 0.005854642018675804,
"physics:principalAxes": "(0.60001326, 0.61213464, 0.36140645, 0.36696142)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.007942971773445606,
3.0191060318429663e-07,
0.0022011229302734137
],
"physics:diagonalInertia": [
2.8543986729800963e-08,
3.1735549299583e-08,
4.670372533155387e-08
],
"physics:mass": 0.0008955339435487986,
"physics:principalAxes": "(0.1595419, 0.6888463, 0.68889564, -0.15956172)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.0011416588677093387,
-3.4777902328642085e-05,
0.02289307489991188
],
"physics:diagonalInertia": [
1.3455648684157495e-07,
6.678977229057637e-07,
6.742146183569275e-07
],
"physics:mass": 0.004509356338530779,
"physics:principalAxes": "(0.51171285, 0.5117713, 0.48780316, 0.48814765)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal/index_middle",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.00020683031470980495,
-2.697628360692761e-06,
0.016705555841326714
],
"physics:diagonalInertia": [
9.46798621725975e-08,
1.7680224573268788e-07,
2.311921036834974e-07
],
"physics:mass": 0.0023786183446645737,
"physics:principalAxes": "(0.74448675, 0.0000263087, 0.6676372, 0.000023150385)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal/index_middle/index_distal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.008162603713572025,
-2.2672160412184894e-05,
0.008118197321891785
],
"physics:diagonalInertia": [
8.608285639866153e-08,
1.7809531982493354e-07,
2.076272522799627e-07
],
"physics:mass": 0.003341762349009514,
"physics:principalAxes": "(0.9024365, 0.0029987681, 0.43081006, 0.0014355157)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.007942971773445606,
3.0191060318429663e-07,
0.0022011229302734137
],
"physics:diagonalInertia": [
2.8543986729800963e-08,
3.1735549299583e-08,
4.670372533155387e-08
],
"physics:mass": 0.0008955339435487986,
"physics:principalAxes": "(0.1595419, 0.6888463, 0.68889564, -0.15956172)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.0011416588677093387,
-3.4777902328642085e-05,
0.02289307489991188
],
"physics:diagonalInertia": [
1.3455648684157495e-07,
6.678977229057637e-07,
6.742146183569275e-07
],
"physics:mass": 0.004509356338530779,
"physics:principalAxes": "(0.51171285, 0.5117713, 0.48780316, 0.48814765)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal/middle_middle",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.00020683031470980495,
-2.697631998671568e-06,
0.016705555841326714
],
"physics:diagonalInertia": [
9.46798621725975e-08,
1.7680224573268788e-07,
2.311921036834974e-07
],
"physics:mass": 0.0023786183446645737,
"physics:principalAxes": "(0.74448675, 0.000026309293, 0.6676372, 0.000023150105)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal/middle_middle/middle_distal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.008162603713572025,
-2.2672160412184894e-05,
0.008118197321891785
],
"physics:diagonalInertia": [
8.608285639866153e-08,
1.7809531982493354e-07,
2.076272522799627e-07
],
"physics:mass": 0.003341762349009514,
"physics:principalAxes": "(0.9024365, 0.0029987684, 0.43081006, 0.0014355157)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.007942971773445606,
3.0191060318429663e-07,
0.0022011229302734137
],
"physics:diagonalInertia": [
2.8543986729800963e-08,
3.1735549299583e-08,
4.670372533155387e-08
],
"physics:mass": 0.0008955339435487986,
"physics:principalAxes": "(0.1595419, 0.6888463, 0.68889564, -0.15956172)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.0011416588677093387,
-3.4777902328642085e-05,
0.02289307489991188
],
"physics:diagonalInertia": [
1.3455648684157495e-07,
6.678977229057637e-07,
6.742146183569275e-07
],
"physics:mass": 0.004509356338530779,
"physics:principalAxes": "(0.51171285, 0.5117713, 0.48780316, 0.48814765)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal/ring_middle",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.00020683031470980495,
-2.697628360692761e-06,
0.016705555841326714
],
"physics:diagonalInertia": [
9.46798621725975e-08,
1.7680224573268788e-07,
2.311921036834974e-07
],
"physics:mass": 0.0023786183446645737,
"physics:principalAxes": "(0.74448675, 0.0000263087, 0.6676372, 0.000023150385)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal/ring_middle/ring_distal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.008162603713572025,
-2.2672344130114652e-05,
0.008118197321891785
],
"physics:diagonalInertia": [
8.608285639866153e-08,
1.7809531982493354e-07,
2.076272522799627e-07
],
"physics:mass": 0.003341762349009514,
"physics:principalAxes": "(0.9024365, 0.0029986603, 0.43081006, 0.0014354715)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.007942971773445606,
3.0191060318429663e-07,
0.0022011229302734137
],
"physics:diagonalInertia": [
2.8543986729800963e-08,
3.1735549299583e-08,
4.670372533155387e-08
],
"physics:mass": 0.0008955339435487986,
"physics:principalAxes": "(0.1595419, 0.6888463, 0.68889564, -0.15956172)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.0011416588677093387,
-3.47779096045997e-05,
0.02289307489991188
],
"physics:diagonalInertia": [
1.3455648684157495e-07,
6.678977229057637e-07,
6.742146183569275e-07
],
"physics:mass": 0.004509356338530779,
"physics:principalAxes": "(0.51171285, 0.5117713, 0.48780316, 0.48814765)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal/pinky_middle",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.00020683031470980495,
-2.697628360692761e-06,
0.016705555841326714
],
"physics:diagonalInertia": [
9.46798621725975e-08,
1.7680224573268788e-07,
2.311921036834974e-07
],
"physics:mass": 0.0023786183446645737,
"physics:principalAxes": "(0.74448675, 0.0000263087, 0.6676372, 0.000023150385)"
}
},
{
"path": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal/pinky_middle/pinky_distal",
"reset_xform_stack": false,
"mass_properties": {
"physics:centerOfMass": [
-0.008162603713572025,
-2.2672205886919983e-05,
0.008118197321891785
],
"physics:diagonalInertia": [
8.608285639866153e-08,
1.7809531982493354e-07,
2.076272522799627e-07
],
"physics:mass": 0.003341762349009514,
"physics:principalAxes": "(0.9024365, 0.0029987409, 0.43081006, 0.0014355045)"
}
}
],
"joints": [
{
"name": "index_dip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/index_dip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal/index_middle",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal/index_middle/index_distal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.5499999681585768,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "index_mcp_pitch",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/index_mcp_pitch",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.219999954319697,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "index_mcp_roll",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/index_mcp_roll",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals",
"axis": "X",
"lower_rad": -0.22999999602684235,
"upper_rad": 0.22999999602684235,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "index_pip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/index_pip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/index_metacarpals/index_proximal/index_middle",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.7500000370721651,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "middle_dip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/middle_dip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal/middle_middle",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal/middle_middle/middle_distal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.5499999681585768,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "middle_mcp_pitch",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/middle_mcp_pitch",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.219999954319697,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "middle_mcp_roll",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/middle_mcp_roll",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals",
"axis": "X",
"lower_rad": -0.22999999602684235,
"upper_rad": 0.22999999602684235,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "middle_pip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/middle_pip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/middle_metacarpals/middle_proximal/middle_middle",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.7500000370721651,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "pinky_dip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/pinky_dip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal/pinky_middle",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal/pinky_middle/pinky_distal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.5499999681585768,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "pinky_mcp_pitch",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/pinky_mcp_pitch",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.219999954319697,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "pinky_mcp_roll",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/pinky_mcp_roll",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals",
"axis": "X",
"lower_rad": -0.22999999602684235,
"upper_rad": 0.22999999602684235,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "pinky_pip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/pinky_pip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/pinky_metacarpals/pinky_proximal/pinky_middle",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.7500000370721651,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "ring_dip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/ring_dip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal/ring_middle",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal/ring_middle/ring_distal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.5499999681585768,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "ring_mcp_pitch",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/ring_mcp_pitch",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.219999954319697,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "ring_mcp_roll",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/ring_mcp_roll",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals",
"axis": "X",
"lower_rad": -0.22999999602684235,
"upper_rad": 0.22999999602684235,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "ring_pip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/ring_pip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/ring_metacarpals/ring_proximal/ring_middle",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.7500000370721651,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "thumb_cmc_pitch",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_cmc_pitch",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 0.8299999863857683,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "thumb_cmc_roll",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_cmc_roll",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1",
"axis": "X",
"lower_rad": 0.0,
"upper_rad": 1.3900000461857607,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "thumb_cmc_yaw",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_cmc_yaw",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2",
"axis": "Z",
"lower_rad": 0.0,
"upper_rad": 1.570000041628963,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "thumb_ip",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_ip",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals/thumb_proximal",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals/thumb_proximal/thumb_distal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.2899999451499395,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
},
{
"name": "thumb_mcp",
"path": "/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_mcp",
"body0": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals",
"body1": "/tn__linkerhand_L25NS_righturdf_td0/Geometry/hand_base_link/thumb_metacarpals_base1/thumb_metacarpals_base2/thumb_metacarpals/thumb_proximal",
"axis": "Y",
"lower_rad": 0.0,
"upper_rad": 1.2500000645252762,
"authored_drive_properties_usd_units": {
"drive:angular:physics:damping": 0.001745329238474369,
"drive:angular:physics:maxForce": 1.0,
"drive:angular:physics:stiffness": 0.05235987901687622,
"drive:angular:physics:type": "force",
"physxJoint:maxJointVelocity": 5729.578125
}
}
],
"source_urdf": {
"status": "STRUCTURAL_MATCH_ONLY",
"file_name": "linkerhand_g20_right.urdf",
"sha256": "aa198155651eb7e815f1c27bc8bd9c902e969e81c1165aed756779db378e715d",
"mimic": [
{
"joint": "index_dip",
"reference": "index_pip",
"multiplier": 0.89,
"offset_rad": 0.0
},
{
"joint": "middle_dip",
"reference": "middle_pip",
"multiplier": 0.89,
"offset_rad": 0.0
},
{
"joint": "pinky_dip",
"reference": "pinky_pip",
"multiplier": 0.89,
"offset_rad": 0.0
},
{
"joint": "ring_dip",
"reference": "ring_pip",
"multiplier": 0.89,
"offset_rad": 0.0
},
{
"joint": "thumb_ip",
"reference": "thumb_mcp",
"multiplier": 1.03,
"offset_rad": 0.0
}
]
},
"coupling_evidence": [
"/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_ip.newton:mimicCoef1",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_ip.newton:mimicJoint",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/thumb_ip: SdfTokenListOp(Explicit Items: [PhysxJointAPI, NewtonMimicAPI, PhysicsDriveAPI:angular, PhysicsJointStateAPI:angular, IsaacJointAPI])",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/index_dip.newton:mimicCoef1",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/index_dip.newton:mimicJoint",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/index_dip: SdfTokenListOp(Explicit Items: [PhysxJointAPI, NewtonMimicAPI, PhysicsDriveAPI:angular, PhysicsJointStateAPI:angular, IsaacJointAPI])",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/middle_dip.newton:mimicCoef1",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/middle_dip.newton:mimicJoint",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/middle_dip: SdfTokenListOp(Explicit Items: [PhysxJointAPI, NewtonMimicAPI, PhysicsDriveAPI:angular, PhysicsJointStateAPI:angular, IsaacJointAPI])",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/ring_dip.newton:mimicCoef1",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/ring_dip.newton:mimicJoint",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/ring_dip: SdfTokenListOp(Explicit Items: [PhysxJointAPI, NewtonMimicAPI, PhysicsDriveAPI:angular, PhysicsJointStateAPI:angular, IsaacJointAPI])",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/pinky_dip.newton:mimicCoef1",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/pinky_dip.newton:mimicJoint",
"/tn__linkerhand_L25NS_righturdf_td0/Physics/pinky_dip: SdfTokenListOp(Explicit Items: [PhysxJointAPI, NewtonMimicAPI, PhysicsDriveAPI:angular, PhysicsJointStateAPI:angular, IsaacJointAPI])"
],
"physx_coupling_evidence": [],
"coupling_status": "MIMIC_SCHEMA_PRESENT_RUNTIME_UNVERIFIED",
"dynamic_replay_ready": false,
"blockers": [
"Runtime mimic response is unverified; original follower drives need suppression for diagnostics.",
"No calibrated wrist support controller or whole-hand dynamic replay validation."
]
}
+110 -10
View File
@@ -2,38 +2,94 @@
Requires inspected PhysX 110.1.13 + current Isaac Lab ProxyArray/xyzw API. Runtime
coupling is tested from passive follower response, not inferred from schema names.
Use an external timeout <=300s. Do not launch alongside another Kit/GPU job.
Use an externally bounded timeout (default budget <=300s; longer runs need approval).
Do not launch alongside another Kit/GPU job.
"""
import argparse
import hashlib
import json
import math
import random
import traceback
from dataclasses import replace
from pathlib import Path
def main():
from isaaclab.app import AppLauncher
def validate_replay_request(steps, full_episode, has_hdf5, workspace_radius):
"""Stdlib-only launch bounds, evaluated before Kit or tracking imports."""
maximum = 32000 if full_episode else 1200
if not isinstance(steps, int) or not 100 <= steps <= maximum:
raise ValueError(f"Require 100 <= steps <= {maximum}")
if full_episode and not has_hdf5:
raise ValueError("Full-episode mode requires --hdf5")
if workspace_radius is not None and (not math.isfinite(workspace_radius) or workspace_radius <= 0):
raise ValueError("Workspace radius must be finite positive")
def validate_replay_duration(steps, dt, duration, full_episode):
"""Reject extrapolation and partial/off-grid full-episode requests before sampling."""
if not steps * dt <= duration:
raise ValueError("Reference shorter than requested steps")
if full_episode and abs(steps * dt - duration) > 1e-9:
raise ValueError("Full episode requires exact on-grid end and steps")
def configure_presentation(args):
"""Resolve opt-in GUI without silently overriding explicit display conflicts."""
if args.gui:
if args.headless:
raise ValueError("--gui conflicts with --headless")
if (getattr(args, "visualizer_explicit", False) or args.visualizer is not None) and args.visualizer != ["kit"]:
raise ValueError("--gui requires the Kit visualizer; conflicts with --visualizer/--viz")
args.headless, args.visualizer = False, ["kit"]
else:
args.headless = True # Preserve the diagnostic's legacy headless default.
if args.visualizer is None:
args.visualizer = "none"
def build_parser(add_app_launcher_args):
"""Keep replay argument parsing testable without importing or launching Kit."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("asset", type=Path, help="Experimental prepared overlay, never original fixed preview")
parser.add_argument("--manifest", type=Path, required=True, help="Original data/source manifest")
parser.add_argument("--hdf5", type=Path, help="Omit for explicitly synthetic diagnostic reference")
parser.add_argument("--episode", default="demo_000000")
parser.add_argument("--steps", type=int, default=480, help="Per repetition at 240Hz; two repetitions")
parser.add_argument(
"--full-episode", action="store_true", help="Require complete HDF5 duration; allow up to 32000 steps"
)
parser.add_argument(
"--workspace-radius", type=float, help="Metres from initial root; overrides only workspace_radius"
)
parser.add_argument("--execute-experimental", action="store_true", help="Acknowledge uncalibrated controller")
parser.add_argument("--limits", type=Path, help="JSON fields of control.Limits, SI units; UNCALIBRATED")
AppLauncher.add_app_launcher_args(parser)
parser.set_defaults(headless=True, visualizer="none")
parser.add_argument("--gui", action="store_true", help="Opt-in bright-hand/dark-backdrop Kit preview")
add_app_launcher_args(parser)
# None distinguishes omitted flags from explicit --headless / --viz none.
parser.set_defaults(headless=None, visualizer=None)
return parser
def main():
from isaaclab.app import AppLauncher
parser = build_parser(AppLauncher.add_app_launcher_args)
args = parser.parse_args()
if not args.execute_experimental or not 100 <= args.steps <= 1200:
parser.error("Require --execute-experimental and 100 <= steps <= 1200 per repetition")
if not args.execute_experimental:
parser.error("Require --execute-experimental")
try:
configure_presentation(args)
validate_replay_request(args.steps, args.full_episode, args.hdf5 is not None, args.workspace_radius)
except ValueError as error:
parser.error(str(error))
# Kit must choose/register its USD libraries before any pxr-dependent imports.
launcher = AppLauncher(args)
app = launcher.app
exit_code = 0
failure_context = {"phase": "setup", "completed_repetitions": 0}
try:
import numpy as np
import torch
@@ -52,11 +108,13 @@ def main():
manifest = json.loads(args.manifest.read_text())
prepared = inspect_prepared(args.asset, manifest)
limits = Limits(**json.loads(args.limits.read_text())) if args.limits else Limits()
if args.workspace_radius is not None:
limits = replace(limits, workspace_radius=args.workspace_radius)
data = load(args.hdf5, manifest) if args.hdf5 else synthetic(manifest)
episode = data.episodes[args.episode]
validate_reference(episode, limits)
dt = 1 / 240
require(args.steps * dt <= episode.time[-1], "Reference shorter than requested steps")
validate_replay_duration(args.steps, dt, episode.time[-1], args.full_episode)
reference = sample(episode, np.arange(args.steps + 1) * dt)
# Hold-start reset uses zero velocities; require a genuinely moving diagnostic
# for every mimic pair, so an ignored/disabled constraint cannot pass at zero.
@@ -119,10 +177,25 @@ def main():
anchor_path = "/World/Hand" + manifest["world_fixed_joints"][0]["path"][len(manifest["default_prim"]) :]
anchor = sim.stage.GetPrimAtPath(anchor_path)
require(anchor.IsValid() and not anchor.IsActive(), "Obsolete world anchor must be inactive")
effective_roots = [p for p in sim.stage.Traverse() if p.HasAPI(UsdPhysics.ArticulationRootAPI)]
require(len(effective_roots) == 1, "Expected exactly one effective articulation root")
require(
str(effective_roots[0].GetPath())
== "/World/Hand" + manifest["root_body_path"][len(manifest["default_prim"]) :],
"Effective articulation root is not the root body",
)
active_joints = [p for p in sim.stage.Traverse() if p.IsA(UsdPhysics.Joint)]
require(len(active_joints) == len(names), "Unexpected active joint count")
require(all(p.IsA(UsdPhysics.RevoluteJoint) for p in active_joints), "Unexpected active constraint")
if args.gui:
from dex_workbench_tracking.preview import create_preview
eye, center = create_preview(sim.stage, episode.wrist_position)
sim.set_camera_view(eye, center)
sim.reset()
if args.gui:
sim.render()
print("GUI_READY: cyan line is the wrist reference; bounded physical replay", flush=True)
require(not hand.is_fixed_base and hand.num_instances == 1, "Expected one floating PhysX articulation")
require(
set(hand.joint_names) == set(names) and len(hand.joint_names) == len(names), "Runtime DOF mapping mismatch"
@@ -157,6 +230,7 @@ def main():
upper = np.array([j["upper_rad"] for j in manifest["joints"]])
traces, reset_states, summaries = [], [], []
for repetition in range(2):
failure_context = {"phase": "reset", "completed_repetitions": repetition, "repetition": repetition}
hand.reset()
hand.permanent_wrench_composer.reset()
hand.instantaneous_wrench_composer.reset()
@@ -184,6 +258,12 @@ def main():
reset_states.append(np.r_[reset_pose, reset_vel, reset_q])
trace, errors = [], []
for step in range(args.steps):
failure_context = {
"phase": "stepping",
"completed_repetitions": repetition,
"repetition": repetition,
"completed_steps_in_repetition": step,
}
require(app.is_running(), "Application stopped before finite test completed")
pose, velocity, q = state()
force, torque = wrench(
@@ -206,11 +286,17 @@ def main():
target=tensor(reference.joint_position[step : step + 1, master_columns]), joint_ids=master_ids
)
hand.write_data_to_sim()
sim.step(render=False)
sim.step(render=args.gui and step % 8 == 0)
hand.update(dt)
pose, velocity, q = state()
qdot = array(hand.data.joint_vel)[0]
failure_context["completed_steps_in_repetition"] = step + 1
require(all(np.isfinite(v).all() for v in (pose, velocity, q, qdot)), "Nonfinite dynamic state")
failure_context["measured_speeds_m_s_rad_s_rad_s"] = [
float(np.linalg.norm(velocity[:3])),
float(np.linalg.norm(velocity[3:])),
float(np.max(np.abs(qdot))),
]
require(
np.linalg.norm(velocity[:3]) < 0.5
and np.linalg.norm(velocity[3:]) < 3
@@ -229,6 +315,12 @@ def main():
position_error = np.linalg.norm(pose[:3] - reference.wrist_position[step + 1])
angle_error = np.linalg.norm(rotation_error(reference.wrist_quaternion[step + 1], pose[3:]))
joint_error = np.max(np.abs(q - reference.joint_position[step + 1]))
failure_context["last_errors_m_rad_rad_rad"] = [
float(position_error),
float(angle_error),
float(joint_error),
float(residual),
]
require(residual < 0.002, "Mimic runtime residual >0.002rad; parser/constraint not verified")
require(
position_error < 0.05 and angle_error < 0.5 and joint_error < 0.2,
@@ -255,6 +347,7 @@ def main():
"rms_errors_m_rad_rad_rad": np.sqrt((errors**2).mean(axis=0)).tolist(),
}
)
failure_context = {"phase": "repeatability", "completed_repetitions": 2, "steps_per_repetition": args.steps}
np.testing.assert_allclose(reset_states[0], reset_states[1], atol=1e-6, rtol=0)
position_end = 7 + len(names)
np.testing.assert_allclose(traces[0][:, :position_end], traces[1][:, :position_end], atol=1e-3, rtol=0)
@@ -272,11 +365,17 @@ def main():
"reference_hdf5_sha256": hashlib.sha256(args.hdf5.read_bytes()).hexdigest() if args.hdf5 else None,
"reference_description": data.metadata["source_description"],
"world_anchor_inactive": not anchor.IsActive(),
"hand_side": data.metadata["hand_side"],
"effective_articulation_root_count": len(effective_roots),
"active_state_joint_count": len(active_joints),
"runtime_is_fixed_base": hand.is_fixed_base,
"seed": 42,
"num_envs": 1,
"steps_per_repetition": args.steps,
"reference_duration_s": float(episode.time[-1]),
"replayed_duration_s": args.steps * dt,
"reference_coverage": "full" if abs(args.steps * dt - episode.time[-1]) <= 1e-9 else "partial",
"full_episode_requested": args.full_episode,
"repetitions": 2,
"physx_version": version,
"prepared": prepared,
@@ -287,10 +386,11 @@ def main():
),
flush=True,
)
except BaseException:
except BaseException as error:
# Kit fast shutdown may not return; emit the failure before closing.
exit_code = 1
traceback.print_exc()
print(json.dumps({"status": "FAIL", "error": str(error), "progress": failure_context}), flush=True)
raise
finally:
app.close(exit_code=exit_code)
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
# Semantic Versioning is used: https://semver.org/
version = "0.1.1"
version = "0.1.2"
# Description
category = "isaaclab"
@@ -42,4 +42,4 @@ name = "dex_workbench.ui_extension_example"
# after this extension is installed with ./isaaclab.sh --install.
# List package names only; version ranges, extras, and platform markers
# come from this extension's setup.py metadata.
# pip_upgrade_dependencies = ["example_package"]
# pip_upgrade_dependencies = ["example_package"]
@@ -52,7 +52,26 @@ def dependencies(source):
return [{"path": name, "sha256": sha} for name, sha in entries], digest.hexdigest()
def inspect(source, source_urdf=None):
def require_static_endpoint(stage, path):
"""Only an identity default Xform can stand for the importer's static world."""
prim = stage.GetPrimAtPath(path)
_require(prim == stage.GetDefaultPrim() and prim.IsA(UsdGeom.Xform), "Unsupported static anchor endpoint")
while prim and not prim.IsPseudoRoot():
_require(
not prim.HasAPI(UsdPhysics.RigidBodyAPI) and not prim.HasAPI(UsdPhysics.ArticulationRootAPI),
"Static endpoint has rigid/articulation API",
)
_require(not any(a.GetTimeSamples() for a in prim.GetAttributes()), "Sampled static endpoint")
xform = UsdGeom.Xformable(prim)
_require(not xform.GetResetXformStack(), "Static endpoint reset stack prohibited")
_require(
Gf.IsClose(UsdGeom.XformCache().GetLocalToWorldTransform(prim), Gf.Matrix4d(1), 1e-12),
"Static endpoint must have identity world transform",
)
prim = prim.GetParent()
def inspect(source, source_urdf=None, hand_side=None):
source = Path(source).resolve(strict=True)
deps, bundle_sha = dependencies(source)
stage = Usd.Stage.Open(str(source))
@@ -86,7 +105,9 @@ def inspect(source, source_urdf=None):
b0 = [str(p) for p in joint.GetBody0Rel().GetTargets()]
b1 = [str(p) for p in joint.GetBody1Rel().GetTargets()]
_require(len(b1) == 1 and b1[0] in body_paths, f"Unresolved/non-body body1: {prim.GetPath()}")
if not b0:
if not b0 or (len(b0) == 1 and b0[0] not in body_paths):
if b0:
require_static_endpoint(stage, b0[0])
_require(prim.IsA(UsdPhysics.FixedJoint), "Unsupported non-fixed world joint")
world_joints.append({"path": str(prim.GetPath()), "body1": b1[0]})
continue
@@ -187,7 +208,14 @@ def inspect(source, source_urdf=None):
coupling_status = "MIMIC_SCHEMA_PRESENT_RUNTIME_UNVERIFIED"
elif source_info["mimic"]:
coupling_status = "MISSING_MIMIC_SCHEMA"
identity = {}
if hand_side is not None:
from .identity import manifest_side
identity = {"hand_side": hand_side}
manifest_side(dict(identity, source_urdf=source_info))
return {
**identity,
"manifest_version": "l20_asset_manifest_v1",
"asset_sha256": bundle_sha,
"entry_file": source.name,
@@ -238,9 +266,11 @@ def prepare_floating_overlay(source, output):
before = inspect(source)
_require(before["physics_variant"] == "physx", "Only the current PhysX variant is supported")
anchors = before["world_fixed_joints"]
_require(len(anchors) == 1, "Expected one fixed-world anchor")
site = before["articulation_roots"][0]
_require(
len(anchors) == 1 and before["articulation_roots"] == [anchors[0]["path"]],
"Expected a fixed-world articulation root; will not guess topology",
site in (anchors[0]["path"], before["root_body_path"].rsplit("/", 1)[0]),
"Expected fixed anchor or exact root container articulation site",
)
original = Usd.Stage.Open(str(source))
stage = Usd.Stage.CreateInMemory()
@@ -252,19 +282,28 @@ def prepare_floating_overlay(source, output):
anchor = stage.GetPrimAtPath(anchors[0]["path"])
# Snapshot resolved schema fallbacks before removing the API. Preserve them
# explicitly on the inert anchor and the new root, not just authored values.
articulation_site = stage.GetPrimAtPath(site)
articulation_values = []
for attr in anchor.GetAttributes():
if attr.GetName().startswith("physxArticulation:") and attr.Get() is not None:
for attr in articulation_site.GetAttributes():
if (
attr.GetName().startswith("physxArticulation:") or attr.GetName() == "newton:selfCollisionEnabled"
) and attr.Get() is not None:
_require(not attr.GetTimeSamples(), f"Time samples prohibited: {attr.GetPath()}")
articulation_values.append((attr.GetName(), attr.GetTypeName(), attr.Get()))
anchor.RemoveAPI(UsdPhysics.ArticulationRootAPI)
anchor.RemoveAppliedSchema("PhysxArticulationAPI")
source_schemas = articulation_site.GetMetadata("apiSchemas").GetAppliedItems()
articulation_site.RemoveAPI(UsdPhysics.ArticulationRootAPI)
articulation_site.RemoveAppliedSchema("PhysxArticulationAPI")
if "NewtonArticulationRootAPI" in source_schemas:
articulation_site.RemoveAppliedSchema("NewtonArticulationRootAPI")
UsdPhysics.Joint(anchor).GetJointEnabledAttr().Set(False)
root = stage.GetPrimAtPath(before["root_body_path"])
UsdPhysics.ArticulationRootAPI.Apply(root)
root.AddAppliedSchema("PhysxArticulationAPI")
if "PhysxArticulationAPI" in source_schemas:
root.AddAppliedSchema("PhysxArticulationAPI")
if "NewtonArticulationRootAPI" in source_schemas:
root.AddAppliedSchema("NewtonArticulationRootAPI")
for name, dtype, value in articulation_values:
anchor.CreateAttribute(name, dtype).Set(value)
articulation_site.CreateAttribute(name, dtype).Set(value)
root.CreateAttribute(name, dtype).Set(value)
stage.GetRootLayer().customLayerData = {"purpose": "L20 topology-only diagnostic; dynamic replay BLOCKED"}
cache0, cache1 = UsdGeom.XformCache(), UsdGeom.XformCache()
@@ -305,15 +344,18 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("asset", type=Path)
parser.add_argument("--source-urdf", type=Path, help="Optional local original; inspected, never modified")
parser.add_argument(
"--hand-side", choices=("left", "right"), help="Explicit inspected identity; right requires source URDF"
)
parser.add_argument("--output", type=Path, required=True, help="New JSON manifest; no overwrite")
parser.add_argument("--floating-overlay", type=Path, help="New topology-only USD layer; no overwrite")
args = parser.parse_args()
try:
_require(not args.output.exists(), "Refusing to overwrite manifest")
manifest = inspect(args.asset, args.source_urdf)
manifest = inspect(args.asset, args.source_urdf, args.hand_side)
if args.floating_overlay:
prepare_floating_overlay(args.asset, args.floating_overlay)
manifest = inspect(args.floating_overlay, args.source_urdf)
manifest = inspect(args.floating_overlay, args.source_urdf, args.hand_side)
args.output.parent.mkdir(parents=True, exist_ok=True)
with args.output.open("x", encoding="utf-8") as stream:
json.dump(manifest, stream, indent=2, allow_nan=False)
@@ -1,6 +1,7 @@
"""CPU preparation CLI: validate, synthesize test references, or resample HDF5."""
import argparse
import hashlib
import json
import os
import tempfile
@@ -9,6 +10,7 @@ from pathlib import Path
import h5py
import numpy as np
from .identity import manifest_side
from .trajectory import (
ContractError,
Demonstrations,
@@ -48,6 +50,26 @@ def publish_validated(path, data, manifest):
os.link(temporary, path)
def stretch_time(data, factor):
"""Slow references without altering any pose, joint, valid mask or coordinate metadata."""
require(np.isfinite(factor) and factor >= 1, "Time stretch factor must be finite and >= 1")
episodes = {}
for name, episode in data.episodes.items():
with np.errstate(over="ignore"):
time = episode.time * factor
require(np.isfinite(time).all(), "Time stretch overflow")
episodes[name] = Episode(
time,
episode.wrist_position.copy(),
episode.wrist_quaternion.copy(),
episode.joint_position.copy(),
episode.valid.copy(),
)
metadata = dict(data.metadata)
metadata["source_description"] += f"; uniform time stretch factor={factor:g}; all original samples preserved"
return Demonstrations(metadata, data.joint_names, data.world_from_source.copy(), episodes)
def synthetic(manifest, profile="small"):
"""Named analytic fixtures, not experts. ``range_4s`` has smooth out-and-back endpoints."""
require(profile in ("small", "range_4s"), "Unknown synthetic profile")
@@ -81,7 +103,7 @@ def synthetic(manifest, profile="small"):
metadata = {
"schema_version": "l20_tracking_v1",
"embodiment": "L20",
"hand_side": "left",
"hand_side": manifest_side(manifest),
"asset_sha256": manifest["asset_sha256"],
"root_link": manifest["root_link"],
"metric_scale_provenance": "Analytic SI fixture, not video reconstruction or measurement",
@@ -124,6 +146,13 @@ def main():
resample.add_argument("--manifest", required=True, type=Path)
resample.add_argument("--hz", required=True, type=float)
resample.add_argument("--output", required=True, type=Path)
stretch = commands.add_parser(
"stretch-time", help="Uniform slowdown; no pose/coordinate changes or identity override"
)
stretch.add_argument("input", type=Path)
stretch.add_argument("--manifest", required=True, type=Path)
stretch.add_argument("--factor", required=True, type=float)
stretch.add_argument("--output", required=True, type=Path)
gate = commands.add_parser("replay-check", help="Fails closed until a physical tracking Adapter is implemented")
gate.add_argument("--manifest", required=True, type=Path)
args = parser.parse_args()
@@ -139,6 +168,12 @@ def main():
publish_validated(args.output, data, manifest)
else:
data = load(args.input, manifest)
if args.command == "stretch-time":
data = stretch_time(data, args.factor)
data.metadata["source_description"] += (
f"; stretch input HDF5 SHA256={hashlib.sha256(args.input.read_bytes()).hexdigest()}"
)
publish_validated(args.output, data, manifest)
if args.command == "resample":
require(np.isfinite(args.hz) and 0 < args.hz <= 1000, "hz must be finite and in (0, 1000]")
episodes = {}
@@ -0,0 +1,32 @@
"""Explicit inspected L20 source identities, not inferred hardware motor layouts."""
LEFT_URDF_SHA = "b8ef22e436ab311fb61f87091b72ae717821092471d628d0ea9475b5088daa5d"
RIGHT_URDF_SHA = "aa198155651eb7e815f1c27bc8bd9c902e969e81c1165aed756779db378e715d"
SOURCE_SHA_BY_SIDE = {"left": LEFT_URDF_SHA, "right": RIGHT_URDF_SHA}
def manifest_side(manifest):
"""Legacy side-less manifests are left only; right requires explicit provenance."""
side = manifest.get("hand_side", "left")
if side not in SOURCE_SHA_BY_SIDE:
raise ValueError("hand_side must be left or right")
sha = manifest.get("source_urdf", {}).get("sha256")
if side == "right" and sha != RIGHT_URDF_SHA:
raise ValueError("Right manifest requires inspected right URDF identity")
if sha in SOURCE_SHA_BY_SIDE.values() and sha != SOURCE_SHA_BY_SIDE[side]:
raise ValueError("Manifest hand_side conflicts with source URDF identity")
return side
def model_mimic(side):
return [
{"joint": f"{finger}_dip", "reference": f"{finger}_pip", "multiplier": 0.89, "offset_rad": 0.0}
for finger in ("index", "middle", "pinky", "ring")
] + [
{
"joint": "thumb_ip",
"reference": "thumb_mcp",
"multiplier": 1.03 if side == "right" else 1.02,
"offset_rad": 0.0,
}
]
@@ -12,14 +12,12 @@ from pathlib import Path
from pxr import Gf, Plug, Sdf, Usd, UsdGeom, UsdPhysics
from .asset import _require, dependencies, inspect, prepare_floating_overlay
from .asset import _require, dependencies, inspect, prepare_floating_overlay, require_static_endpoint
from .identity import LEFT_URDF_SHA, SOURCE_SHA_BY_SIDE, manifest_side, model_mimic
SOURCE_URDF_SHA = "b8ef22e436ab311fb61f87091b72ae717821092471d628d0ea9475b5088daa5d"
SOURCE_URDF_SHA = LEFT_URDF_SHA
BACKEND_VERSION = "110.1.13"
MIMIC = [
{"joint": f"{finger}_dip", "reference": f"{finger}_pip", "multiplier": 0.89, "offset_rad": 0.0}
for finger in ("index", "middle", "pinky", "ring")
] + [{"joint": "thumb_ip", "reference": "thumb_mcp", "multiplier": 1.02, "offset_rad": 0.0}]
MIMIC = model_mimic("left") # Existing left callers retain the same constant.
DRIVE_FIELDS = ("stiffness", "damping", "maxForce")
@@ -33,7 +31,8 @@ def schema_context():
"""Resolved schema fallbacks are part of the authoring/inspection contract."""
registry = Usd.SchemaRegistry()
return {
name: bool(registry.FindAppliedAPIPrimDefinition(name)) for name in ("PhysxArticulationAPI", "NewtonMimicAPI")
name: bool(registry.FindAppliedAPIPrimDefinition(name))
for name in ("PhysxArticulationAPI", "NewtonMimicAPI", "NewtonArticulationRootAPI")
}
@@ -71,11 +70,17 @@ def newton_mimic(prim):
def bind_source(source, manifest):
"""Reject stale identity/structural metadata before authoring or accepting an overlay."""
side = manifest_side(manifest)
actual = inspect(source)
actual["hand_side"] = side
for key in ("asset_sha256", "joints", "root_body_path", "root_link", "dependencies", "bodies"):
_require(actual[key] == manifest[key], f"Stale/inconsistent source manifest: {key}")
_require(manifest["source_urdf"]["sha256"] == SOURCE_URDF_SHA, "Unapproved URDF provenance")
_require(manifest["source_urdf"]["mimic"] == MIMIC, "Unexpected L20 mimic equations")
_require(manifest["source_urdf"]["sha256"] == SOURCE_SHA_BY_SIDE[side], "Unapproved URDF provenance")
_require(manifest["source_urdf"]["mimic"] == model_mimic(side), "Unexpected L20 mimic equations")
anchors = actual["world_fixed_joints"]
_require(len(anchors) == 1, "Expected one source world anchor")
expected_site = anchors[0]["path"] if side == "left" else actual["default_prim"] + "/Geometry"
_require(actual["articulation_roots"] == [expected_site], "Unexpected source articulation container")
_require(actual["root_link"] == "hand_base_link" and len(actual["joints"]) == 21, "Wrong L20 model")
_require(actual["physics_variant"] == "physx", "PhysX variant required")
validate_mimic(Usd.Stage.Open(str(source)), actual)
@@ -84,7 +89,8 @@ def bind_source(source, manifest):
def validate_mimic(stage, manifest, passive=False):
joints = {j["name"]: j for j in manifest["joints"]}
followers = {eq["joint"] for eq in MIMIC}
equations = model_mimic(manifest.get("hand_side", "left"))
followers = {eq["joint"] for eq in equations}
for name, joint in joints.items():
prim = stage.GetPrimAtPath(joint["path"])
_require(not any("PhysxMimic" in s or "Tendon" in s for s in schemas(prim)), "Duplicate/unsupported coupling")
@@ -93,7 +99,7 @@ def validate_mimic(stage, manifest, passive=False):
_require(joint["axis"] in ("X", "Y", "Z"), "Invalid revolute axis")
if name not in followers:
continue
equation = next(eq for eq in MIMIC if eq["joint"] == name)
equation = next(eq for eq in equations if eq["joint"] == name)
leader, coefficient, offset = newton_mimic(prim)
_require(leader == joints[equation["reference"]]["path"], "Wrong mimic leader")
_require(abs(coefficient - equation["multiplier"]) < 1e-6, "Wrong mimic multiplier/sign")
@@ -107,7 +113,10 @@ def validate_mimic(stage, manifest, passive=False):
def _preserved(original, stage, before):
"""Only leaf world-anchor deactivation, root migration and follower drive suppression are allowed."""
followers = {j["path"] for j in before["joints"] if j["name"] in {eq["joint"] for eq in MIMIC}}
followers = {
j["path"] for j in before["joints"] if j["name"] in {eq["joint"] for eq in model_mimic(before["hand_side"])}
}
site = before["articulation_roots"][0]
anchor = before["world_fixed_joints"][0]["path"]
root = before["root_body_path"]
source_anchor = original.GetPrimAtPath(anchor)
@@ -115,7 +124,11 @@ def _preserved(original, stage, before):
source_anchor.IsA(UsdPhysics.FixedJoint) and not source_anchor.GetAllChildren(), "Expected leaf world anchor"
)
joint = UsdPhysics.Joint(source_anchor)
_require(not joint.GetBody0Rel().GetTargets(), "Anchor must attach directly to world")
targets = joint.GetBody0Rel().GetTargets()
if targets:
_require(len(targets) == 1 and before["hand_side"] == "right", "Unexpected anchor endpoint")
require_static_endpoint(original, targets[0])
require_static_endpoint(stage, targets[0])
_require(joint.GetBody1Rel().GetTargets() == [Sdf.Path(root)], "Anchor must attach only to root")
c0, c1 = UsdGeom.XformCache(), UsdGeom.XformCache()
# TraverseAll retains the deliberately inactive leaf: no other deletion or
@@ -133,17 +146,22 @@ def _preserved(original, stage, before):
if path == anchor:
_require(after.GetAttribute("physics:jointEnabled").Get() is False, "Inactive anchor must remain disabled")
allowed.add("physics:jointEnabled")
expected -= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI"}
if path == site:
expected -= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI", "NewtonArticulationRootAPI"}
if path == root:
expected |= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI"}
expected.add("PhysicsArticulationRootAPI")
if "PhysxArticulationAPI" in schemas(original.GetPrimAtPath(site)):
expected.add("PhysxArticulationAPI")
if "NewtonArticulationRootAPI" in schemas(original.GetPrimAtPath(site)):
expected.add("NewtonArticulationRootAPI")
allowed |= {
a.GetName()
for a in stage.GetPrimAtPath(anchor).GetAttributes()
if a.GetName().startswith("physxArticulation:")
for a in stage.GetPrimAtPath(site).GetAttributes()
if a.GetName().startswith("physxArticulation:") or a.GetName() == "newton:selfCollisionEnabled"
}
for name in allowed:
_require(
after.GetAttribute(name).Get() == stage.GetPrimAtPath(anchor).GetAttribute(name).Get(),
after.GetAttribute(name).Get() == stage.GetPrimAtPath(site).GetAttribute(name).Get(),
"Root solver property changed",
)
if path in followers:
@@ -184,13 +202,17 @@ def inspect_prepared(output, source_manifest):
source = (output.parent / layer.subLayerPaths[0]).resolve(strict=True)
before = bind_source(source, source_manifest)
_require(layer.customLayerData.get("source_bundle_sha256") == before["asset_sha256"], "Overlay provenance mismatch")
_require(layer.customLayerData.get("source_urdf_sha256") == SOURCE_URDF_SHA, "Overlay URDF mismatch")
_require(
layer.customLayerData.get("source_urdf_sha256") == SOURCE_SHA_BY_SIDE[before["hand_side"]],
"Overlay URDF mismatch",
)
_require(
dict(layer.customLayerData.get("schema_context", {})) == schema_context(),
"Authoring schema context mismatch; regenerate from source with --schema-plugin-path for both "
"installed PhysX and Newton plugins, and use the same plugins when inspecting",
)
after = inspect(output)
after["hand_side"] = before["hand_side"]
_require(
not after["world_fixed_joints"] and after["articulation_roots"] == [before["root_body_path"]], "Not floating"
)
@@ -208,7 +230,8 @@ def inspect_prepared(output, source_manifest):
"prepared_asset_sha256": after["asset_sha256"],
"source_asset_sha256": before["asset_sha256"],
"independent_joint_names": masters,
"mimic": MIMIC,
"mimic": model_mimic(before["hand_side"]),
"hand_side": before["hand_side"],
"required_physx_version": BACKEND_VERSION,
"root_body_path": before["root_body_path"],
"runtime_verified": False,
@@ -225,7 +248,7 @@ def prepare(source, output, manifest):
prepare_floating_overlay(source, temporary)
stage = Usd.Stage.Open(str(temporary))
for joint in before["joints"]:
if joint["name"] not in {eq["joint"] for eq in MIMIC}:
if joint["name"] not in {eq["joint"] for eq in model_mimic(before["hand_side"])}:
continue
prim = stage.GetPrimAtPath(joint["path"])
# Preserve resolved drive defaults as inert attributes when removing API.
@@ -246,7 +269,7 @@ def prepare(source, output, manifest):
stage.GetRootLayer().customLayerData = {
"purpose": "L20 experimental floating tracking; runtime UNVERIFIED",
"source_bundle_sha256": before["asset_sha256"],
"source_urdf_sha256": SOURCE_URDF_SHA,
"source_urdf_sha256": SOURCE_SHA_BY_SIDE[before["hand_side"]],
"physx_evidence_version": BACKEND_VERSION,
"schema_context": schema_context(),
}
@@ -0,0 +1,63 @@
"""Presentation-only USD preview; import at runtime only after AppLauncher.
CPU USD tests may use this without Kit. No robot materials or physics are edited.
"""
import numpy as np
from pxr import Gf, Sdf, UsdGeom, UsdLux, UsdPhysics, UsdShade
def create_preview(stage, wrist_position):
"""Add the approved contrast preview and return its fixed camera eye/target."""
dome = UsdLux.DomeLight.Define(stage, "/World/PreviewLight")
dome.CreateIntensityAttr(700.0)
dome.CreateColorAttr(Gf.Vec3f(1.0, 1.0, 1.0))
key = UsdLux.DistantLight.Define(stage, "/World/PreviewKey")
key.CreateIntensityAttr(800.0)
key.CreateAngleAttr(12.0)
UsdGeom.Xformable(key.GetPrim()).AddRotateXYZOp().Set(Gf.Vec3f(-35, 25, 25))
fill = UsdLux.DistantLight.Define(stage, "/World/PreviewFill")
fill.CreateIntensityAttr(300.0)
fill.CreateAngleAttr(20.0)
UsdGeom.Xformable(fill.GetPrim()).AddRotateXYZOp().Set(Gf.Vec3f(50, -40, -120))
curve = UsdGeom.BasisCurves.Define(stage, "/World/WristReferencePath")
curve.CreateTypeAttr("linear")
curve.CreateCurveVertexCountsAttr([len(wrist_position)])
curve.CreatePointsAttr([Gf.Vec3f(*map(float, p)) for p in wrist_position])
curve.CreateWidthsAttr([0.002])
curve.SetWidthsInterpolation("constant")
curve.CreateDisplayColorAttr([Gf.Vec3f(0.1, 0.8, 1.0)])
center = (wrist_position.min(axis=0) + wrist_position.max(axis=0)) / 2
eye = center + np.array([1.15, -1.35, 0.95])
normal = (eye - center) / np.linalg.norm(eye - center)
right = np.cross(normal, np.array([0.0, 0.0, 1.0]))
right /= np.linalg.norm(right)
up = np.cross(normal, right)
backdrop_center = center - 2.0 * normal
backdrop = UsdGeom.Mesh.Define(stage, "/World/PreviewBackdrop")
backdrop.CreatePointsAttr(
[
Gf.Vec3f(*map(float, backdrop_center + 5.0 * (x * right + y * up)))
for x, y in [(-1, -1), (1, -1), (1, 1), (-1, 1)]
]
)
backdrop.CreateFaceVertexCountsAttr([4])
backdrop.CreateFaceVertexIndicesAttr([0, 1, 2, 3])
backdrop.CreateSubdivisionSchemeAttr("none")
backdrop.CreateDoubleSidedAttr(True)
# Display-only backdrop: no collision/rigid-body API, constant emission
# separates background brightness from the hand's environment lighting.
material = UsdShade.Material.Define(stage, "/World/PreviewBackdropMaterial")
shader = UsdShade.Shader.Define(stage, "/World/PreviewBackdropMaterial/Surface")
shader.CreateIdAttr("UsdPreviewSurface")
shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0.0))
shader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0.055, 0.065, 0.08))
shader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
shader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0.0))
shader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(1.0)
material.CreateSurfaceOutput().ConnectToSource(shader.ConnectableAPI(), "surface")
UsdShade.MaterialBindingAPI.Apply(backdrop.GetPrim()).Bind(material)
assert not backdrop.GetPrim().HasAPI(UsdPhysics.CollisionAPI)
assert not backdrop.GetPrim().HasAPI(UsdPhysics.RigidBodyAPI)
return tuple(eye), tuple(center)
@@ -7,6 +7,8 @@ from pathlib import Path
import h5py
import numpy as np
from .identity import manifest_side
class ContractError(ValueError):
"""An input cannot be safely interpreted as a reference trajectory."""
@@ -70,8 +72,9 @@ def load(path: str | Path, manifest: dict | None = None) -> Demonstrations:
):
require(name in file.attrs, f"missing root attribute {name}")
attributes[name] = text(file.attrs[name], name)
for name, expected in (("schema_version", "l20_tracking_v1"), ("embodiment", "L20"), ("hand_side", "left")):
for name, expected in (("schema_version", "l20_tracking_v1"), ("embodiment", "L20")):
require(attributes[name] == expected, f"{name}: expected {expected}")
require(attributes["hand_side"] in ("left", "right"), "hand_side must be left or right")
require(
re.fullmatch(r"[0-9a-f]{64}", attributes["asset_sha256"]) is not None,
"asset_sha256: lowercase SHA-256 required",
@@ -150,6 +153,7 @@ def load(path: str | Path, manifest: dict | None = None) -> Demonstrations:
def validate_against_manifest(data: Demonstrations, manifest: dict):
"""Check identity/order/limits and known URDF mimic equations, not actuator validity."""
require(manifest.get("manifest_version") == "l20_asset_manifest_v1", "unsupported manifest")
require(data.metadata["hand_side"] == manifest_side(manifest), "hand_side mismatch")
require(data.metadata["asset_sha256"] == manifest["asset_sha256"], "asset bundle hash mismatch")
require(data.metadata["root_link"] == manifest["root_link"], "root link mismatch")
joints = manifest["joints"]
+48
View File
@@ -4,6 +4,54 @@ Changelog
Unreleased
~~~~~~~~~~
0.1.2 (2026-09-14)
~~~~~~~~~~~~~~~~~~
Local bilateral tracking and bounded expert-reference milestone, not full release
clearance. Right USDs and expert data are local-only and NOT bundled: clean-clone
right-asset tests/replay remain blocked pending storage/licensing resolution.
Full pre-commit is unavailable; consolidated GUI E2E has not been run.
Cartpole and its task ID are unchanged; older prepared overlays need regeneration.
Added
^^^^^
* Add opt-in ``--gui`` to the existing replay runner, with the approved bright-hand
lighting, cyan wrist path, fixed camera and dark non-physical backdrop in a small
presentation helper. Keep default headless execution and 240Hz controls unchanged;
render every eight physics steps only in GUI mode. Add CPU display/flag regressions.
The separate GUI clone was visually approved; this consolidated CLI is NOT_RUN in Kit.
* Add explicit workspace-radius override and finite full-episode HDF5 replay
(at most 32000 steps per repetition), retaining the default 0.1m/1200-step bounds
and all controller/safety thresholds. Add strict duration and preservation tests.
* Add identity-validated, no-overwrite ``stretch-time`` preparation that preserves
original samples, geometry, coordinate metadata and expert provenance; record
the factor and input SHA256. No generic asset-hash override or original-speed claim.
* Complete one separately authorized 600s-budget recovery replay: right expert
reference uniformly slowed 80x, full 2x32000 steps at 240Hz, exit0/PASS in 384s;
81 CPU regressions pass. Preserve the interrupted run as unknown and optional
interpolated-grid equality audit failures; no runtime safety thresholds relaxed.
* Convert the user-provided L20 RIGHT URDF with the specified Isaac Lab converter;
record source provenance and right manifest for its self-contained local USD bundle
(generated USD files are excluded from this commit).
Geometry exceeds the ordinary-Git 2000KB gate: publication remains blocked
pending approved LFS/external storage and redistribution licensing confirmation.
* Extend the existing tracking contract with explicit right-side identity and its
inspected thumb mimic coefficient 1.03; preserve legacy left data/manifest support.
Add cross-side, axis, relocation and strict importer-topology regressions.
* Validate right small 2x480 and synthetic real-file HDF5 2x960 steps, plus left
small 2x480 regression, with unchanged controls and assertions. Those earlier
synthetic runs are not expert-data evidence; the separate slowed expert run is above.
Fixed
^^^^^
* Handle the exact right importer static default-Xform anchor and Geometry root
site. Migrate source-present Newton articulation API too, because its registered
schema inherits PhysicsArticulationRootAPI; preserve resolved source properties.
Check exactly one effective runtime root. Schema context now includes Newton
articulation registration; older prepared overlays must be regenerated.
0.1.1 (2026-09-11)
~~~~~~~~~~~~~~~~~~
@@ -81,6 +81,29 @@ class ControlTests(unittest.TestCase):
with self.assertRaises(ContractError):
Limits(force=value)
def test_workspace_extension_does_not_relax_velocity_or_forces(self):
from dataclasses import replace
manifest = {
"manifest_version": "l20_asset_manifest_v1",
"asset_sha256": "a" * 64,
"root_link": "test",
"joints": [{"name": "a", "lower_rad": 0, "upper_rad": 1}],
}
episode = synthetic(manifest).episodes["demo_000000"]
episode.time[:] *= 80
episode.wrist_position[:, 0] *= 300
with self.assertRaisesRegex(ContractError, "outside workspace"):
validate_reference(episode, self.limits)
extended = replace(self.limits, workspace_radius=0.8)
validate_reference(episode, extended)
for key, value in vars(self.limits).items():
if key != "workspace_radius":
self.assertEqual(getattr(extended, key), value)
episode.time[:] /= 80
with self.assertRaisesRegex(ContractError, "too fast"):
validate_reference(episode, extended)
def test_reference_envelope_and_invalid_gap(self):
manifest = {
"manifest_version": "l20_asset_manifest_v1",
@@ -0,0 +1,86 @@
"""CPU-only regression for the approved display scene; never launch Kit."""
import unittest
import numpy as np
from dex_workbench_tracking.preview import create_preview
from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux, UsdPhysics, UsdShade
class TrackingPreviewTests(unittest.TestCase):
def setUp(self):
self.stage = Usd.Stage.CreateInMemory()
UsdGeom.Xform.Define(self.stage, "/World")
hand = UsdGeom.Mesh.Define(self.stage, "/World/Hand")
UsdPhysics.RigidBodyAPI.Apply(hand.GetPrim())
UsdPhysics.CollisionAPI.Apply(hand.GetPrim())
UsdPhysics.ArticulationRootAPI.Apply(hand.GetPrim())
material = UsdShade.Material.Define(self.stage, "/World/Hand/Material")
UsdShade.MaterialBindingAPI.Apply(hand.GetPrim()).Bind(material)
self.path = np.array([[0.1, -0.2, 0.4], [0.7, 0.1, -0.2], [0.2, 0.3, 0.1]])
def hand_spec(self):
layer = Sdf.Layer.CreateAnonymous()
Sdf.CopySpec(self.stage.GetRootLayer(), "/World/Hand", layer, "/Hand")
return layer.ExportToString()
def test_preview_never_changes_robot_materials_or_physics(self):
before = self.hand_spec()
create_preview(self.stage, self.path)
self.assertEqual(self.hand_spec(), before)
for prim in self.stage.Traverse():
if str(prim.GetPath()).startswith("/World/Hand"):
continue
with self.subTest(prim=prim.GetPath()):
for api in (UsdPhysics.RigidBodyAPI, UsdPhysics.CollisionAPI, UsdPhysics.ArticulationRootAPI):
self.assertFalse(prim.HasAPI(api))
self.assertFalse(prim.IsA(UsdPhysics.Joint))
self.assertFalse(any(a.GetTimeSamples() for a in prim.GetAttributes()))
bindings = [p for p in self.stage.Traverse() if p.HasAPI(UsdShade.MaterialBindingAPI)]
self.assertEqual({str(p.GetPath()) for p in bindings}, {"/World/Hand", "/World/PreviewBackdrop"})
def test_lighting_and_linear_cyan_reference_match_approved_preview(self):
original = self.path.copy()
create_preview(self.stage, self.path)
np.testing.assert_array_equal(self.path, original)
dome = UsdLux.DomeLight(self.stage.GetPrimAtPath("/World/PreviewLight"))
self.assertEqual(dome.GetIntensityAttr().Get(), 700)
self.assertEqual(dome.GetColorAttr().Get(), Gf.Vec3f(1))
for name, intensity, angle, rotation in (
("PreviewKey", 800, 12, (-35, 25, 25)),
("PreviewFill", 300, 20, (50, -40, -120)),
):
light = UsdLux.DistantLight(self.stage.GetPrimAtPath("/World/" + name))
self.assertEqual(light.GetIntensityAttr().Get(), intensity)
self.assertEqual(light.GetAngleAttr().Get(), angle)
self.assertEqual(light.GetPrim().GetAttribute("xformOp:rotateXYZ").Get(), Gf.Vec3f(*rotation))
curve = UsdGeom.BasisCurves(self.stage.GetPrimAtPath("/World/WristReferencePath"))
self.assertEqual(curve.GetTypeAttr().Get(), "linear")
self.assertEqual(list(curve.GetCurveVertexCountsAttr().Get()), [3])
np.testing.assert_allclose(curve.GetPointsAttr().Get(), self.path, atol=1e-7)
np.testing.assert_allclose(curve.GetWidthsAttr().Get(), [0.002])
self.assertEqual(curve.GetWidthsInterpolation(), "constant")
self.assertEqual(list(curve.GetDisplayColorAttr().Get()), [Gf.Vec3f(0.1, 0.8, 1)])
def test_fixed_camera_and_dark_camera_facing_backdrop(self):
eye, center = create_preview(self.stage, self.path)
np.testing.assert_array_equal(center, (self.path.min(axis=0) + self.path.max(axis=0)) / 2)
np.testing.assert_allclose(np.array(eye) - center, [1.15, -1.35, 0.95])
normal = (np.array(eye) - center) / np.linalg.norm(np.array(eye) - center)
mesh = UsdGeom.Mesh(self.stage.GetPrimAtPath("/World/PreviewBackdrop"))
points = np.array(mesh.GetPointsAttr().Get())
np.testing.assert_allclose(points.mean(axis=0), np.array(center) - 2 * normal, atol=5e-7)
np.testing.assert_allclose((points - center) @ normal, -2, atol=5e-7)
self.assertEqual(list(mesh.GetFaceVertexCountsAttr().Get()), [4])
self.assertEqual(list(mesh.GetFaceVertexIndicesAttr().Get()), [0, 1, 2, 3])
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), "none")
self.assertTrue(mesh.GetDoubleSidedAttr().Get())
material, _ = UsdShade.MaterialBindingAPI(mesh.GetPrim()).ComputeBoundMaterial()
self.assertEqual(str(material.GetPath()), "/World/PreviewBackdropMaterial")
shader = UsdShade.Shader(self.stage.GetPrimAtPath("/World/PreviewBackdropMaterial/Surface"))
self.assertEqual(shader.GetIdAttr().Get(), "UsdPreviewSurface")
self.assertEqual(shader.GetInput("diffuseColor").Get(), Gf.Vec3f(0))
self.assertEqual(shader.GetInput("emissiveColor").Get(), Gf.Vec3f(0.055, 0.065, 0.08))
self.assertEqual(shader.GetInput("specularColor").Get(), Gf.Vec3f(0))
self.assertEqual(shader.GetInput("roughness").Get(), 1)
@@ -0,0 +1,129 @@
"""Stdlib prelaunch bounds; no Kit import or launch."""
import argparse
import ast
import importlib.util
import unittest
from pathlib import Path
spec = importlib.util.spec_from_file_location(
"track_l20_options", Path(__file__).resolve().parents[3] / "scripts/tracking/track_l20.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
validate_replay_request = module.validate_replay_request
class TrackingReplayTests(unittest.TestCase):
def presentation_args(self, *options):
class ExplicitVisualizer(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
namespace.visualizer_explicit = True
def add_launcher_args(parser):
# Match AppLauncher: explicit 'none' becomes None, not ['none'].
# The separate marker distinguishes disabling from omitting --viz.
parser.add_argument("--headless", action="store_true")
parser.add_argument(
"--visualizer",
"--viz",
type=lambda value: None if value == "none" else value.split(","),
action=ExplicitVisualizer,
)
parser = module.build_parser(add_launcher_args)
args = parser.parse_args(["hand.usda", "--manifest", "manifest.json", *options])
module.configure_presentation(args)
return args
def test_gui_opt_in_and_legacy_headless_defaults(self):
for options in ((), ("--headless",), ("--viz", "none")):
args = self.presentation_args(*options)
self.assertFalse(args.gui)
self.assertTrue(args.headless)
self.assertIn(args.visualizer, ("none", ["none"]))
self.assertEqual(args.steps, 480)
self.assertIsNone(args.workspace_radius)
self.assertFalse(args.full_episode)
for options in (("--gui",), ("--gui", "--viz", "kit"), ("--visualizer=kit", "--gui")):
args = self.presentation_args(*options)
self.assertTrue(args.gui)
self.assertFalse(args.headless)
self.assertEqual(args.visualizer, ["kit"])
self.assertEqual(args.steps, 480)
def test_gui_rejects_explicit_display_conflicts(self):
for options in (
("--gui", "--headless"),
("--headless", "--gui"),
("--gui", "--viz", "none"),
("--visualizer=none", "--gui"),
("--gui", "--viz", "newton"),
):
with self.subTest(options=options), self.assertRaisesRegex(ValueError, "conflicts"):
self.presentation_args(*options)
def test_explicit_none_is_not_an_omitted_visualizer(self):
omitted = self.presentation_args("--gui")
self.assertFalse(getattr(omitted, "visualizer_explicit", False))
disabled = self.presentation_args("--viz", "none")
self.assertTrue(disabled.visualizer_explicit)
self.assertTrue(disabled.headless)
with self.assertRaisesRegex(ValueError, "conflicts"):
module.configure_presentation(
argparse.Namespace(gui=True, headless=None, visualizer=None, visualizer_explicit=True)
)
def test_render_cadence_is_gui_only_and_does_not_change_physics_dt(self):
tree = ast.parse(Path(module.__file__).read_text())
steps = [
n
for n in ast.walk(tree)
if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and ast.unparse(n.func) == "sim.step"
]
self.assertEqual(len(steps), 1)
expression = steps[0].keywords[0]
self.assertEqual(expression.arg, "render")
self.assertEqual(ast.unparse(expression.value), "args.gui and step % 8 == 0")
self.assertEqual(
[
ast.unparse(n.value)
for n in ast.walk(tree)
if isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "dt" for t in n.targets)
],
["1 / 240"],
)
def test_full_episode_duration_rejects_partial_off_grid_and_extrapolation(self):
validate = module.validate_replay_duration
validate(480, 1 / 240, 2.0, False)
validate(480, 1 / 240, 4.0, False) # Legacy bounded partial diagnostic.
validate(32000, 1 / 240, (50 / 30) * 80, True)
for duration in (134.0, 32000 / 240 + 0.001):
with self.subTest(duration=duration), self.assertRaisesRegex(ValueError, "exact on-grid"):
validate(32000, 1 / 240, duration, True)
with self.assertRaisesRegex(ValueError, "exact on-grid"):
validate(480, 1 / 240, 4.0, True)
for full in (False, True):
for duration in (1.0, float("nan")):
with self.subTest(full=full, duration=duration), self.assertRaisesRegex(ValueError, "shorter"):
validate(480, 1 / 240, duration, full)
def test_finite_launch_bounds_and_full_episode_opt_in(self):
validate_replay_request(480, False, False, None)
validate_replay_request(1200, False, True, None)
validate_replay_request(32000, True, True, 0.8)
for args in (
(32000, False, True, 0.8),
(1201, False, True, None),
(32001, True, True, 0.8),
(99, False, True, 0.8),
(480, True, False, 0.8),
(float("inf"), True, True, 0.8),
):
with self.subTest(args=args), self.assertRaises(ValueError):
validate_replay_request(*args)
for value in (0, -1, float("nan"), float("inf")):
with self.subTest(radius=value), self.assertRaises(ValueError):
validate_replay_request(480, False, True, value)
@@ -0,0 +1,150 @@
"""Actual right asset, bilateral identity and preservation regressions; no Kit."""
import copy
import json
import shutil
import tempfile
import unittest
from pathlib import Path
import numpy as np
from dex_workbench_tracking.asset import dependencies, inspect
from dex_workbench_tracking.cli import publish_validated, synthetic
from dex_workbench_tracking.control import Limits, validate_reference
from dex_workbench_tracking.identity import manifest_side
from dex_workbench_tracking.prepared import inspect_prepared, prepare
from dex_workbench_tracking.trajectory import load, validate_against_manifest
from pxr import Gf, Usd, UsdGeom, UsdPhysics
ASSETS = Path(__file__).resolve().parents[3] / "assets/robots/dex_hand"
class TrackingRightTests(unittest.TestCase):
def setUp(self):
self.right = json.loads((ASSETS / "linkerhand_g20_right/tracking_manifest.json").read_text())
self.left = json.loads((ASSETS / "linkerhand_g20_left/tracking_manifest.json").read_text())
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.directory = Path(self.temp.name)
self.bundle = self.directory / "bundle"
shutil.copytree(ASSETS / "linkerhand_g20_right/linkerhand_g20_right", self.bundle / "original")
self.source = self.bundle / "original/linkerhand_g20_right.usda"
self.output = self.bundle / "tracking.usda"
def test_right_profiles_roundtrip_limits_and_source_mimic(self):
for profile, count in (("small", 121), ("range_4s", 241)):
data = synthetic(self.right, profile)
self.assertEqual(data.metadata["hand_side"], "right")
self.assertEqual(data.metadata["provenance"], "synthetic")
path = self.directory / (profile + ".hdf5")
publish_validated(path, data, self.right)
episode = load(path, self.right).episodes["demo_000000"]
self.assertEqual(len(episode.time), count)
validate_reference(episode, Limits())
names = list(data.joint_names)
np.testing.assert_allclose(
episode.joint_position[:, names.index("thumb_ip")],
1.03 * episode.joint_position[:, names.index("thumb_mcp")],
atol=1e-7,
)
with self.assertRaises(FileExistsError):
publish_validated(path, data, self.right)
def test_cross_side_rejected_even_with_forged_hash_and_root(self):
for source, target in ((self.left, self.right), (self.right, self.left)):
data = synthetic(source)
data.metadata["asset_sha256"] = target["asset_sha256"]
data.metadata["root_link"] = target["root_link"]
with self.assertRaisesRegex(ValueError, "hand_side"):
validate_against_manifest(data, target)
def test_right_identity_cannot_be_missing_or_relabelled(self):
for side in (None, "left", "both"):
wrong = copy.deepcopy(self.right)
if side is None:
del wrong["hand_side"]
else:
wrong["hand_side"] = side
with self.assertRaises(ValueError):
manifest_side(wrong)
wrong = copy.deepcopy(self.left)
wrong["hand_side"] = "right"
with self.assertRaises(ValueError):
manifest_side(wrong)
self.assertEqual(manifest_side(self.left), "left")
def test_actual_right_axes_limits_and_relocated_dependencies(self):
actual = inspect(self.source)
self.assertEqual(actual["asset_sha256"], self.right["asset_sha256"])
self.assertEqual(actual["joints"], self.right["joints"])
self.assertEqual(len(actual["bodies"]), 22)
self.assertEqual(len(actual["joints"]), 21)
for dep in actual["dependencies"]:
self.assertTrue((self.source.parent / dep["path"]).resolve().is_relative_to(self.bundle))
stage = Usd.Stage.Open(str(self.source))
for joint in actual["joints"]:
name = joint["name"]
expected = (0, 0, -1) if name == "thumb_cmc_yaw" else ((1, 0, 0) if name.endswith("roll") else (0, 1, 0))
rotation = stage.GetPrimAtPath(joint["path"]).GetAttribute("physics:localRot1").Get()
axis = Gf.Vec3d(*(float(joint["axis"] == c) for c in "XYZ"))
np.testing.assert_allclose(Gf.Rotation(Gf.Quatd(rotation)).TransformDir(axis), expected, atol=1e-6)
before = dependencies(self.source)
result = prepare(self.source, self.output, self.right)
self.assertEqual(len(result["independent_joint_names"]), 16)
self.assertEqual(result["mimic"][-1]["multiplier"], 1.03)
moved = self.directory / "moved"
shutil.copytree(self.bundle, moved)
self.assertEqual(result, inspect_prepared(moved / "tracking.usda", self.right))
self.assertEqual(before, dependencies(self.source))
def test_modified_static_endpoint_transform_api_reset_or_target_rejected(self):
for change in ("transform", "sample", "rigid", "reset", "target"):
with self.subTest(change=change):
stage = Usd.Stage.Open(str(self.source))
stage.SetEditTarget(stage.GetRootLayer())
prim = stage.GetDefaultPrim()
if change == "transform":
UsdGeom.Xformable(prim).AddTranslateOp().Set(Gf.Vec3d(1, 0, 0))
elif change == "sample":
UsdGeom.Xformable(prim).AddTranslateOp().Set(Gf.Vec3d(0), Usd.TimeCode(1))
elif change == "rigid":
UsdPhysics.RigidBodyAPI.Apply(prim)
elif change == "reset":
UsdGeom.Xformable(prim).SetResetXformStack(True)
else:
anchor = stage.GetPrimAtPath(self.right["world_fixed_joints"][0]["path"])
UsdPhysics.Joint(anchor).GetBody0Rel().SetTargets([self.right["default_prim"] + "/Geometry"])
stage.GetRootLayer().Save()
with self.assertRaises(ValueError):
inspect(self.source)
# Only restore this test's private copy; reload to clear USD cache.
shutil.copyfile(
ASSETS / "linkerhand_g20_right/linkerhand_g20_right/linkerhand_g20_right.usda", self.source
)
stage.Reload()
def test_overlay_rejects_reintroduced_newton_root_and_changed_container(self):
prepare(self.source, self.output, self.right)
stage = Usd.Stage.Open(str(self.output))
site = stage.GetPrimAtPath(self.right["articulation_roots"][0])
site.AddAppliedSchema("NewtonArticulationRootAPI")
stage.GetRootLayer().Save()
with self.assertRaises(ValueError):
inspect_prepared(self.output, self.right)
def test_source_container_must_be_exact_geometry_parent(self):
stage = Usd.Stage.Open(str(self.source))
site = stage.GetPrimAtPath(self.right["articulation_roots"][0])
site.RemoveAPI(UsdPhysics.ArticulationRootAPI)
site.RemoveAppliedSchema("NewtonArticulationRootAPI")
wrong = stage.DefinePrim(self.right["default_prim"] + "/Wrong", "Xform")
UsdPhysics.ArticulationRootAPI.Apply(wrong)
stage.GetRootLayer().Save()
manifest = dict(inspect(self.source), hand_side="right", source_urdf=self.right["source_urdf"])
with self.assertRaisesRegex(ValueError, "container"):
prepare(self.source, self.output, manifest)
if __name__ == "__main__":
unittest.main()
@@ -16,6 +16,11 @@ ASSET = REPO / "assets/robots/dex_hand/linkerhand_g20_left"
class TrackingSchemaContextTests(unittest.TestCase):
def test_registered_schema_preparation_preserves_defaults_and_rejects_context_drift(self):
for side in ("left", "right"):
with self.subTest(side=side):
self.check_registered(REPO / f"assets/robots/dex_hand/linkerhand_g20_{side}", side)
def check_registered(self, asset, side):
installation = os.environ.get("ISAAC_PATH")
if not installation:
self.skipTest("ISAAC_PATH required for installed PhysX/Newton CPU schema regression")
@@ -24,8 +29,8 @@ class TrackingSchemaContextTests(unittest.TestCase):
self.assertEqual(len(physx), 1, "Select an unambiguous installed PhysX schema for this test")
newton = installation / "exts/omni.usd.schema.newton/usd/schema/newton/newton_usd_schemas/plugInfo.json"
self.assertTrue(newton.is_file())
manifest = json.loads((ASSET / "tracking_manifest.json").read_text())
source = ASSET / "linkerhand_g20_left/linkerhand_g20_left.usda"
manifest = json.loads((asset / "tracking_manifest.json").read_text())
source = asset / f"linkerhand_g20_{side}/linkerhand_g20_{side}.usda"
with tempfile.TemporaryDirectory() as directory:
directory = Path(directory)
unregistered = directory / "unregistered.usda"
@@ -37,7 +42,7 @@ class TrackingSchemaContextTests(unittest.TestCase):
code = """
import json, sys
from pathlib import Path
from pxr import Usd
from pxr import Usd, UsdPhysics
from dex_workbench_tracking.asset import dependencies
from dex_workbench_tracking.prepared import inspect_prepared, register_schema_plugins
source, manifest_path, unregistered, registered, *plugins = sys.argv[1:]
@@ -64,17 +69,24 @@ assert all(result["schema_context"].values())
assert dependencies(source) == before
original = Usd.Stage.Open(source)
stage = Usd.Stage.Open(registered)
anchor = manifest["world_fixed_joints"][0]["path"]
site = manifest["articulation_roots"][0]
root = stage.GetPrimAtPath(manifest["root_body_path"])
for attr in original.GetPrimAtPath(anchor).GetAttributes():
if attr.GetName().startswith("physxArticulation:"):
for prim in (stage.GetPrimAtPath(anchor), root):
roots = [p for p in stage.Traverse() if p.HasAPI(UsdPhysics.ArticulationRootAPI)]
assert roots == [root], roots
if manifest.get("hand_side") == "right":
assert not stage.GetPrimAtPath(site).HasAPI(UsdPhysics.ArticulationRootAPI)
assert "NewtonArticulationRootAPI" not in stage.GetPrimAtPath(site).GetAppliedSchemas()
assert "NewtonArticulationRootAPI" in root.GetAppliedSchemas()
for attr in original.GetPrimAtPath(site).GetAttributes():
if attr.GetName().startswith("physxArticulation:") or attr.GetName() == "newton:selfCollisionEnabled":
for prim in (stage.GetPrimAtPath(site), root):
after = prim.GetAttribute(attr.GetName())
assert after.Get() == attr.Get(), attr.GetPath()
assert after.HasAuthoredValue(), attr.GetPath()
assert not after.GetTimeSamples(), attr.GetPath()
# Registered schema fallbacks do not relax the static/time-sample gate.
root.GetAttribute("physxArticulation:sleepThreshold").Set(99, Usd.TimeCode(1))
field = "newton:selfCollisionEnabled" if manifest.get("hand_side") == "right" else "physxArticulation:sleepThreshold"
root.GetAttribute(field).Set(True if manifest.get("hand_side") == "right" else 99, Usd.TimeCode(1))
stage.GetRootLayer().Save()
try:
inspect_prepared(registered, manifest)
@@ -82,7 +94,7 @@ except ValueError as error:
assert "Time samples prohibited" in str(error), error
else:
raise AssertionError("Sampled solver property accepted")
root.GetAttribute("physxArticulation:sleepThreshold").ClearAtTime(Usd.TimeCode(1))
root.GetAttribute(field).ClearAtTime(Usd.TimeCode(1))
stage.GetRootLayer().Save()
inspect_prepared(registered, manifest)
print("PASS: isolated registered schema author/inspect, exact default preservation, strict time samples")
@@ -93,7 +105,7 @@ print("PASS: isolated registered schema author/inspect, exact default preservati
"-c",
code,
str(source),
str(ASSET / "tracking_manifest.json"),
str(asset / "tracking_manifest.json"),
str(unregistered),
str(registered),
str(physx[0]),
@@ -11,7 +11,7 @@ from pathlib import Path
import h5py
import numpy as np
from dex_workbench_tracking.cli import publish_validated, synthetic, write
from dex_workbench_tracking.cli import publish_validated, stretch_time, synthetic, write
from dex_workbench_tracking.trajectory import ContractError, load, require_dynamic_replay_ready, sample
@@ -38,6 +38,68 @@ class TrackingTrajectoryTests(unittest.TestCase):
with h5py.File(self.path, "r+") as file:
function(file)
def test_stretch_time_preserves_samples_identity_and_input(self):
original = load(self.path, self.manifest)
original.metadata["provenance"] = "expert_retargeted"
original.world_from_source[:3, :3] = [[1, 0, 0], [0, 0, 1], [0, -1, 0]]
source_bytes = self.path.read_bytes()
result = stretch_time(original, 80)
self.assertEqual(result.metadata["provenance"], "expert_retargeted")
self.assertEqual(result.metadata["asset_sha256"], original.metadata["asset_sha256"])
self.assertEqual(result.joint_names, original.joint_names)
np.testing.assert_array_equal(result.world_from_source, original.world_from_source)
for name, episode in original.episodes.items():
stretched = result.episodes[name]
np.testing.assert_array_equal(stretched.time, episode.time * 80)
for field in ("wrist_position", "wrist_quaternion", "joint_position", "valid"):
np.testing.assert_array_equal(getattr(stretched, field), getattr(episode, field))
self.assertEqual(episode.time[-1], 2)
destination = Path(self.directory.name) / "stretched.hdf5"
publish_validated(destination, result, self.manifest)
self.assertEqual(load(destination, self.manifest).episodes["demo_000000"].time[-1], 160)
with self.assertRaises(FileExistsError):
publish_validated(destination, result, self.manifest)
self.assertEqual(self.path.read_bytes(), source_bytes)
for name, episode in original.episodes.items():
query = np.linspace(0, episode.time[-1], 401)
expected = sample(episode, query)
actual = sample(result.episodes[name], query * 80)
for field in ("wrist_position", "wrist_quaternion", "joint_position"):
np.testing.assert_allclose(getattr(actual, field), getattr(expected, field), rtol=0, atol=1e-7)
def test_stretch_time_rejects_nonfinite_acceleration_and_overflow(self):
original = load(self.path, self.manifest)
for factor in (0, -1, 0.5, float("nan"), float("inf"), np.finfo(float).max):
with self.subTest(factor=factor), self.assertRaises(ContractError):
stretch_time(original, factor)
def test_stretch_time_cli_identity_and_provenance(self):
manifest_path = Path(self.directory.name) / "manifest.json"
manifest_path.write_text(json.dumps(self.manifest))
destination = Path(self.directory.name) / "stretched.hdf5"
command = [
sys.executable,
"-m",
"dex_workbench_tracking.cli",
"stretch-time",
str(self.path),
"--manifest",
str(manifest_path),
"--factor",
"80",
"--output",
str(destination),
]
result = subprocess.run(command, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("stretch input HDF5 SHA256=", load(destination, self.manifest).metadata["source_description"])
self.assertNotEqual(subprocess.run(command, capture_output=True).returncode, 0)
bad_destination = Path(self.directory.name) / "bad.hdf5"
command[-1] = str(bad_destination)
self.edit(lambda file: file.attrs.__setitem__("asset_sha256", "b" * 64))
self.assertNotEqual(subprocess.run(command, capture_output=True).returncode, 0)
self.assertFalse(bad_destination.exists())
def test_load_roundtrip(self):
data = load(self.path, self.manifest)
self.assertEqual(data.joint_names, ("finger_a", "finger_b"))
@@ -68,7 +130,7 @@ class TrackingTrajectoryTests(unittest.TestCase):
for name, bad in (
("schema_version", "v2"),
("embodiment", "L30"),
("hand_side", "right"),
("hand_side", "both"),
("provenance", "unknown"),
("asset_sha256", "xyz"),
("root_link", ""),
@@ -274,7 +336,7 @@ class TrackingTrajectoryTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout)["asset_compatibility"], "NOT_CHECKED")
self.edit(lambda file: file.attrs.__setitem__("hand_side", "right"))
result = self.run_cli("validate", self.path)
result = self.run_cli("validate", self.path, "--manifest", manifest)
self.assertNotEqual(result.returncode, 0)
self.assertIn("FAIL", result.stderr)