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:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
**当前用途:浮动腕部与全部手指参考关节的轨迹回放,后续用于学习策略。**
|
**当前用途:浮动腕部与全部手指参考关节的轨迹回放,后续用于学习策略。**
|
||||||
|
|
||||||
|
本文第1–9节保持左手交付约定;右手接入差异见第10节,不得直接使用左手哈希、限位或拇指联动。
|
||||||
本文是可独立分享的数据交付说明,与当前仓库校验器一致。它不是通用 HDF5 标准,也不是完整 DexSchema。
|
本文是可独立分享的数据交付说明,与当前仓库校验器一致。它不是通用 HDF5 标准,也不是完整 DexSchema。
|
||||||
请先交付 **1 条短样例**,确认坐标、尺度、关节映射后再批量导出。
|
请先交付 **1 条短样例**,确认坐标、尺度、关节映射后再批量导出。
|
||||||
|
|
||||||
@@ -215,3 +216,28 @@ export PYTHONPATH="$PWD/source/dex_workbench${PYTHONPATH:+:$PYTHONPATH}"
|
|||||||
不是 HDF5 格式要求。合法的静止或局部手指轨迹不应为了满足诊断而添加伪造动作。
|
不是 HDF5 格式要求。合法的静止或局部手指轨迹不应为了满足诊断而添加伪造动作。
|
||||||
|
|
||||||
仿真实现与已有验证结果另见 [`L20_TRACKING.md`](L20_TRACKING.md)。本文只约定数据交付,不要求数据方运行 Isaac Sim。
|
仿真实现与已有验证结果另见 [`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
@@ -1,5 +1,11 @@
|
|||||||
# L20 左手轨迹跟踪:数据交付与准备入口
|
# 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 环境。
|
提供 **CPU 数据接口、资产清单、浮动控制覆盖层和实验性动力学跟踪入口**,不是完成的 RL 环境。
|
||||||
@@ -254,7 +260,8 @@ PYTHONPATH=source/dex_workbench timeout 300s ~/isaacsim/python.sh \
|
|||||||
|
|
||||||
### 控制与验收边界
|
### 控制与验收边界
|
||||||
|
|
||||||
- 单环境、seed42、240Hz;默认两次各480步,单次允许100–1200步,外部 timeout 300秒。
|
- 单环境、seed42、240Hz;默认两次各480步,普通模式单次允许100–1200步,外部 timeout 300秒。
|
||||||
|
显式完整文件模式的有限步数/预算例外见第5节;不会自动扩大默认范围。
|
||||||
只创建手,无物体、桌面、任务注册或训练。不逐步 teleport;仅 reset 写 root/joint 初值并清零速度、
|
只创建手,无物体、桌面、任务注册或训练。不逐步 teleport;仅 reset 写 root/joint 初值并清零速度、
|
||||||
wrench 与目标缓冲、轨迹时间,之后只发 master q_target 和 root COM wrench。
|
wrench 与目标缓冲、轨迹时间,之后只发 master q_target 和 root COM wrench。
|
||||||
- 世界系 PD 跟踪 link 原点;输出作用在 root COM,补上 `(p_link-p_rootCOM)×F_PD`。
|
- 世界系 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 actuator(5个被动关节)、protobuf重复注册、visualizer/MaterialX与usdrt警告;
|
仍有TGS外力迭代、16/21 actuator(5个被动关节)、protobuf重复注册、visualizer/MaterialX与usdrt警告;
|
||||||
未关闭日志或放宽检查。更大范围、负向/多轴腕部运动、接触、真实专家文件、多环境、训练与硬件均未验证。
|
未关闭日志或放宽检查。更大范围、负向/多轴腕部运动、接触、真实专家文件、多环境、训练与硬件均未验证。
|
||||||
完整 pre-commit 仍因未安装工具而 NOT_RUN;本次独立审查仍由父会话执行。
|
完整 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有效。
|
||||||
|
|
||||||
|
### 恢复后的单次授权完整回放:PASS(2026-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强度700,key/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` 轴为负Z;USD规范轴Z配合 `localRot1` 后为负Z。
|
||||||
|
全部21条变换后的轴与源URDF核对通过;不能只看USD的轴字母判断正负。
|
||||||
|
- 源 `root_joint` 是叶 fixed joint,body0为非刚体且世界变换为单位阵的 default Xform,body1为根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、RTX5080,seed42、单环境、240Hz,无物体/桌面/训练。
|
||||||
|
完整预算4次 Kit 启动,每次外部timeout300s:转换、右small、右HDF5、左small回归;均退出0。
|
||||||
|
回放三次各有明确 PASS JSON、两轮完整步数、reset/位置及速度重复性、有限值/限位/被动非零运动检查。
|
||||||
|
三次均报告浮动根、21个活动状态关节、1个有效根、旧锚点inactive;没有重复/嵌套根或CreateJoint警告。
|
||||||
|
|
||||||
|
| 用例 | 每轮步数 × 轮数 | 最大腕位置误差 m | 最大腕姿态误差 rad | 最大关节误差 rad | 最大联动残差 rad |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 右small(2mmX、.01rad yaw、2s) | 480×2 | .000366063 | .002292019 | .009957471 | .000525197 |
|
||||||
|
| 右range_4s HDF5(10mmX、.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/1Nm;master驱动3/.1,effort.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
@@ -2,38 +2,94 @@
|
|||||||
|
|
||||||
Requires inspected PhysX 110.1.13 + current Isaac Lab ProxyArray/xyzw API. Runtime
|
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.
|
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 argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import random
|
import random
|
||||||
import traceback
|
import traceback
|
||||||
|
from dataclasses import replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def validate_replay_request(steps, full_episode, has_hdf5, workspace_radius):
|
||||||
from isaaclab.app import AppLauncher
|
"""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 = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("asset", type=Path, help="Experimental prepared overlay, never original fixed preview")
|
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("--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("--hdf5", type=Path, help="Omit for explicitly synthetic diagnostic reference")
|
||||||
parser.add_argument("--episode", default="demo_000000")
|
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("--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("--execute-experimental", action="store_true", help="Acknowledge uncalibrated controller")
|
||||||
parser.add_argument("--limits", type=Path, help="JSON fields of control.Limits, SI units; UNCALIBRATED")
|
parser.add_argument("--limits", type=Path, help="JSON fields of control.Limits, SI units; UNCALIBRATED")
|
||||||
AppLauncher.add_app_launcher_args(parser)
|
parser.add_argument("--gui", action="store_true", help="Opt-in bright-hand/dark-backdrop Kit preview")
|
||||||
parser.set_defaults(headless=True, visualizer="none")
|
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()
|
args = parser.parse_args()
|
||||||
if not args.execute_experimental or not 100 <= args.steps <= 1200:
|
if not args.execute_experimental:
|
||||||
parser.error("Require --execute-experimental and 100 <= steps <= 1200 per repetition")
|
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.
|
# Kit must choose/register its USD libraries before any pxr-dependent imports.
|
||||||
launcher = AppLauncher(args)
|
launcher = AppLauncher(args)
|
||||||
app = launcher.app
|
app = launcher.app
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
|
failure_context = {"phase": "setup", "completed_repetitions": 0}
|
||||||
try:
|
try:
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
@@ -52,11 +108,13 @@ def main():
|
|||||||
manifest = json.loads(args.manifest.read_text())
|
manifest = json.loads(args.manifest.read_text())
|
||||||
prepared = inspect_prepared(args.asset, manifest)
|
prepared = inspect_prepared(args.asset, manifest)
|
||||||
limits = Limits(**json.loads(args.limits.read_text())) if args.limits else Limits()
|
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)
|
data = load(args.hdf5, manifest) if args.hdf5 else synthetic(manifest)
|
||||||
episode = data.episodes[args.episode]
|
episode = data.episodes[args.episode]
|
||||||
validate_reference(episode, limits)
|
validate_reference(episode, limits)
|
||||||
dt = 1 / 240
|
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)
|
reference = sample(episode, np.arange(args.steps + 1) * dt)
|
||||||
# Hold-start reset uses zero velocities; require a genuinely moving diagnostic
|
# Hold-start reset uses zero velocities; require a genuinely moving diagnostic
|
||||||
# for every mimic pair, so an ignored/disabled constraint cannot pass at zero.
|
# 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_path = "/World/Hand" + manifest["world_fixed_joints"][0]["path"][len(manifest["default_prim"]) :]
|
||||||
anchor = sim.stage.GetPrimAtPath(anchor_path)
|
anchor = sim.stage.GetPrimAtPath(anchor_path)
|
||||||
require(anchor.IsValid() and not anchor.IsActive(), "Obsolete world anchor must be inactive")
|
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)]
|
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(len(active_joints) == len(names), "Unexpected active joint count")
|
||||||
require(all(p.IsA(UsdPhysics.RevoluteJoint) for p in active_joints), "Unexpected active constraint")
|
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()
|
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(not hand.is_fixed_base and hand.num_instances == 1, "Expected one floating PhysX articulation")
|
||||||
require(
|
require(
|
||||||
set(hand.joint_names) == set(names) and len(hand.joint_names) == len(names), "Runtime DOF mapping mismatch"
|
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"]])
|
upper = np.array([j["upper_rad"] for j in manifest["joints"]])
|
||||||
traces, reset_states, summaries = [], [], []
|
traces, reset_states, summaries = [], [], []
|
||||||
for repetition in range(2):
|
for repetition in range(2):
|
||||||
|
failure_context = {"phase": "reset", "completed_repetitions": repetition, "repetition": repetition}
|
||||||
hand.reset()
|
hand.reset()
|
||||||
hand.permanent_wrench_composer.reset()
|
hand.permanent_wrench_composer.reset()
|
||||||
hand.instantaneous_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])
|
reset_states.append(np.r_[reset_pose, reset_vel, reset_q])
|
||||||
trace, errors = [], []
|
trace, errors = [], []
|
||||||
for step in range(args.steps):
|
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")
|
require(app.is_running(), "Application stopped before finite test completed")
|
||||||
pose, velocity, q = state()
|
pose, velocity, q = state()
|
||||||
force, torque = wrench(
|
force, torque = wrench(
|
||||||
@@ -206,11 +286,17 @@ def main():
|
|||||||
target=tensor(reference.joint_position[step : step + 1, master_columns]), joint_ids=master_ids
|
target=tensor(reference.joint_position[step : step + 1, master_columns]), joint_ids=master_ids
|
||||||
)
|
)
|
||||||
hand.write_data_to_sim()
|
hand.write_data_to_sim()
|
||||||
sim.step(render=False)
|
sim.step(render=args.gui and step % 8 == 0)
|
||||||
hand.update(dt)
|
hand.update(dt)
|
||||||
pose, velocity, q = state()
|
pose, velocity, q = state()
|
||||||
qdot = array(hand.data.joint_vel)[0]
|
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")
|
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(
|
require(
|
||||||
np.linalg.norm(velocity[:3]) < 0.5
|
np.linalg.norm(velocity[:3]) < 0.5
|
||||||
and np.linalg.norm(velocity[3:]) < 3
|
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])
|
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:]))
|
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]))
|
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(residual < 0.002, "Mimic runtime residual >0.002rad; parser/constraint not verified")
|
||||||
require(
|
require(
|
||||||
position_error < 0.05 and angle_error < 0.5 and joint_error < 0.2,
|
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(),
|
"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)
|
np.testing.assert_allclose(reset_states[0], reset_states[1], atol=1e-6, rtol=0)
|
||||||
position_end = 7 + len(names)
|
position_end = 7 + len(names)
|
||||||
np.testing.assert_allclose(traces[0][:, :position_end], traces[1][:, :position_end], atol=1e-3, rtol=0)
|
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_hdf5_sha256": hashlib.sha256(args.hdf5.read_bytes()).hexdigest() if args.hdf5 else None,
|
||||||
"reference_description": data.metadata["source_description"],
|
"reference_description": data.metadata["source_description"],
|
||||||
"world_anchor_inactive": not anchor.IsActive(),
|
"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),
|
"active_state_joint_count": len(active_joints),
|
||||||
"runtime_is_fixed_base": hand.is_fixed_base,
|
"runtime_is_fixed_base": hand.is_fixed_base,
|
||||||
"seed": 42,
|
"seed": 42,
|
||||||
"num_envs": 1,
|
"num_envs": 1,
|
||||||
"steps_per_repetition": args.steps,
|
"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,
|
"repetitions": 2,
|
||||||
"physx_version": version,
|
"physx_version": version,
|
||||||
"prepared": prepared,
|
"prepared": prepared,
|
||||||
@@ -287,10 +386,11 @@ def main():
|
|||||||
),
|
),
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
except BaseException:
|
except BaseException as error:
|
||||||
# Kit fast shutdown may not return; emit the failure before closing.
|
# Kit fast shutdown may not return; emit the failure before closing.
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
print(json.dumps({"status": "FAIL", "error": str(error), "progress": failure_context}), flush=True)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
app.close(exit_code=exit_code)
|
app.close(exit_code=exit_code)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
|
|
||||||
# Semantic Versioning is used: https://semver.org/
|
# Semantic Versioning is used: https://semver.org/
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
category = "isaaclab"
|
category = "isaaclab"
|
||||||
|
|||||||
@@ -52,7 +52,26 @@ def dependencies(source):
|
|||||||
return [{"path": name, "sha256": sha} for name, sha in entries], digest.hexdigest()
|
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)
|
source = Path(source).resolve(strict=True)
|
||||||
deps, bundle_sha = dependencies(source)
|
deps, bundle_sha = dependencies(source)
|
||||||
stage = Usd.Stage.Open(str(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()]
|
b0 = [str(p) for p in joint.GetBody0Rel().GetTargets()]
|
||||||
b1 = [str(p) for p in joint.GetBody1Rel().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()}")
|
_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")
|
_require(prim.IsA(UsdPhysics.FixedJoint), "Unsupported non-fixed world joint")
|
||||||
world_joints.append({"path": str(prim.GetPath()), "body1": b1[0]})
|
world_joints.append({"path": str(prim.GetPath()), "body1": b1[0]})
|
||||||
continue
|
continue
|
||||||
@@ -187,7 +208,14 @@ def inspect(source, source_urdf=None):
|
|||||||
coupling_status = "MIMIC_SCHEMA_PRESENT_RUNTIME_UNVERIFIED"
|
coupling_status = "MIMIC_SCHEMA_PRESENT_RUNTIME_UNVERIFIED"
|
||||||
elif source_info["mimic"]:
|
elif source_info["mimic"]:
|
||||||
coupling_status = "MISSING_MIMIC_SCHEMA"
|
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 {
|
return {
|
||||||
|
**identity,
|
||||||
"manifest_version": "l20_asset_manifest_v1",
|
"manifest_version": "l20_asset_manifest_v1",
|
||||||
"asset_sha256": bundle_sha,
|
"asset_sha256": bundle_sha,
|
||||||
"entry_file": source.name,
|
"entry_file": source.name,
|
||||||
@@ -238,9 +266,11 @@ def prepare_floating_overlay(source, output):
|
|||||||
before = inspect(source)
|
before = inspect(source)
|
||||||
_require(before["physics_variant"] == "physx", "Only the current PhysX variant is supported")
|
_require(before["physics_variant"] == "physx", "Only the current PhysX variant is supported")
|
||||||
anchors = before["world_fixed_joints"]
|
anchors = before["world_fixed_joints"]
|
||||||
|
_require(len(anchors) == 1, "Expected one fixed-world anchor")
|
||||||
|
site = before["articulation_roots"][0]
|
||||||
_require(
|
_require(
|
||||||
len(anchors) == 1 and before["articulation_roots"] == [anchors[0]["path"]],
|
site in (anchors[0]["path"], before["root_body_path"].rsplit("/", 1)[0]),
|
||||||
"Expected a fixed-world articulation root; will not guess topology",
|
"Expected fixed anchor or exact root container articulation site",
|
||||||
)
|
)
|
||||||
original = Usd.Stage.Open(str(source))
|
original = Usd.Stage.Open(str(source))
|
||||||
stage = Usd.Stage.CreateInMemory()
|
stage = Usd.Stage.CreateInMemory()
|
||||||
@@ -252,19 +282,28 @@ def prepare_floating_overlay(source, output):
|
|||||||
anchor = stage.GetPrimAtPath(anchors[0]["path"])
|
anchor = stage.GetPrimAtPath(anchors[0]["path"])
|
||||||
# Snapshot resolved schema fallbacks before removing the API. Preserve them
|
# Snapshot resolved schema fallbacks before removing the API. Preserve them
|
||||||
# explicitly on the inert anchor and the new root, not just authored values.
|
# explicitly on the inert anchor and the new root, not just authored values.
|
||||||
|
articulation_site = stage.GetPrimAtPath(site)
|
||||||
articulation_values = []
|
articulation_values = []
|
||||||
for attr in anchor.GetAttributes():
|
for attr in articulation_site.GetAttributes():
|
||||||
if attr.GetName().startswith("physxArticulation:") and attr.Get() is not None:
|
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()}")
|
_require(not attr.GetTimeSamples(), f"Time samples prohibited: {attr.GetPath()}")
|
||||||
articulation_values.append((attr.GetName(), attr.GetTypeName(), attr.Get()))
|
articulation_values.append((attr.GetName(), attr.GetTypeName(), attr.Get()))
|
||||||
anchor.RemoveAPI(UsdPhysics.ArticulationRootAPI)
|
source_schemas = articulation_site.GetMetadata("apiSchemas").GetAppliedItems()
|
||||||
anchor.RemoveAppliedSchema("PhysxArticulationAPI")
|
articulation_site.RemoveAPI(UsdPhysics.ArticulationRootAPI)
|
||||||
|
articulation_site.RemoveAppliedSchema("PhysxArticulationAPI")
|
||||||
|
if "NewtonArticulationRootAPI" in source_schemas:
|
||||||
|
articulation_site.RemoveAppliedSchema("NewtonArticulationRootAPI")
|
||||||
UsdPhysics.Joint(anchor).GetJointEnabledAttr().Set(False)
|
UsdPhysics.Joint(anchor).GetJointEnabledAttr().Set(False)
|
||||||
root = stage.GetPrimAtPath(before["root_body_path"])
|
root = stage.GetPrimAtPath(before["root_body_path"])
|
||||||
UsdPhysics.ArticulationRootAPI.Apply(root)
|
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:
|
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)
|
root.CreateAttribute(name, dtype).Set(value)
|
||||||
stage.GetRootLayer().customLayerData = {"purpose": "L20 topology-only diagnostic; dynamic replay BLOCKED"}
|
stage.GetRootLayer().customLayerData = {"purpose": "L20 topology-only diagnostic; dynamic replay BLOCKED"}
|
||||||
cache0, cache1 = UsdGeom.XformCache(), UsdGeom.XformCache()
|
cache0, cache1 = UsdGeom.XformCache(), UsdGeom.XformCache()
|
||||||
@@ -305,15 +344,18 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("asset", type=Path)
|
parser.add_argument("asset", type=Path)
|
||||||
parser.add_argument("--source-urdf", type=Path, help="Optional local original; inspected, never modified")
|
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("--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")
|
parser.add_argument("--floating-overlay", type=Path, help="New topology-only USD layer; no overwrite")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
_require(not args.output.exists(), "Refusing to overwrite manifest")
|
_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:
|
if args.floating_overlay:
|
||||||
prepare_floating_overlay(args.asset, 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)
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with args.output.open("x", encoding="utf-8") as stream:
|
with args.output.open("x", encoding="utf-8") as stream:
|
||||||
json.dump(manifest, stream, indent=2, allow_nan=False)
|
json.dump(manifest, stream, indent=2, allow_nan=False)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""CPU preparation CLI: validate, synthesize test references, or resample HDF5."""
|
"""CPU preparation CLI: validate, synthesize test references, or resample HDF5."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -9,6 +10,7 @@ from pathlib import Path
|
|||||||
import h5py
|
import h5py
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from .identity import manifest_side
|
||||||
from .trajectory import (
|
from .trajectory import (
|
||||||
ContractError,
|
ContractError,
|
||||||
Demonstrations,
|
Demonstrations,
|
||||||
@@ -48,6 +50,26 @@ def publish_validated(path, data, manifest):
|
|||||||
os.link(temporary, path)
|
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"):
|
def synthetic(manifest, profile="small"):
|
||||||
"""Named analytic fixtures, not experts. ``range_4s`` has smooth out-and-back endpoints."""
|
"""Named analytic fixtures, not experts. ``range_4s`` has smooth out-and-back endpoints."""
|
||||||
require(profile in ("small", "range_4s"), "Unknown synthetic profile")
|
require(profile in ("small", "range_4s"), "Unknown synthetic profile")
|
||||||
@@ -81,7 +103,7 @@ def synthetic(manifest, profile="small"):
|
|||||||
metadata = {
|
metadata = {
|
||||||
"schema_version": "l20_tracking_v1",
|
"schema_version": "l20_tracking_v1",
|
||||||
"embodiment": "L20",
|
"embodiment": "L20",
|
||||||
"hand_side": "left",
|
"hand_side": manifest_side(manifest),
|
||||||
"asset_sha256": manifest["asset_sha256"],
|
"asset_sha256": manifest["asset_sha256"],
|
||||||
"root_link": manifest["root_link"],
|
"root_link": manifest["root_link"],
|
||||||
"metric_scale_provenance": "Analytic SI fixture, not video reconstruction or measurement",
|
"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("--manifest", required=True, type=Path)
|
||||||
resample.add_argument("--hz", required=True, type=float)
|
resample.add_argument("--hz", required=True, type=float)
|
||||||
resample.add_argument("--output", required=True, type=Path)
|
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 = commands.add_parser("replay-check", help="Fails closed until a physical tracking Adapter is implemented")
|
||||||
gate.add_argument("--manifest", required=True, type=Path)
|
gate.add_argument("--manifest", required=True, type=Path)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -139,6 +168,12 @@ def main():
|
|||||||
publish_validated(args.output, data, manifest)
|
publish_validated(args.output, data, manifest)
|
||||||
else:
|
else:
|
||||||
data = load(args.input, manifest)
|
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":
|
if args.command == "resample":
|
||||||
require(np.isfinite(args.hz) and 0 < args.hz <= 1000, "hz must be finite and in (0, 1000]")
|
require(np.isfinite(args.hz) and 0 < args.hz <= 1000, "hz must be finite and in (0, 1000]")
|
||||||
episodes = {}
|
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 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"
|
BACKEND_VERSION = "110.1.13"
|
||||||
MIMIC = [
|
MIMIC = model_mimic("left") # Existing left callers retain the same constant.
|
||||||
{"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}]
|
|
||||||
DRIVE_FIELDS = ("stiffness", "damping", "maxForce")
|
DRIVE_FIELDS = ("stiffness", "damping", "maxForce")
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +31,8 @@ def schema_context():
|
|||||||
"""Resolved schema fallbacks are part of the authoring/inspection contract."""
|
"""Resolved schema fallbacks are part of the authoring/inspection contract."""
|
||||||
registry = Usd.SchemaRegistry()
|
registry = Usd.SchemaRegistry()
|
||||||
return {
|
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):
|
def bind_source(source, manifest):
|
||||||
"""Reject stale identity/structural metadata before authoring or accepting an overlay."""
|
"""Reject stale identity/structural metadata before authoring or accepting an overlay."""
|
||||||
|
side = manifest_side(manifest)
|
||||||
actual = inspect(source)
|
actual = inspect(source)
|
||||||
|
actual["hand_side"] = side
|
||||||
for key in ("asset_sha256", "joints", "root_body_path", "root_link", "dependencies", "bodies"):
|
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(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"]["sha256"] == SOURCE_SHA_BY_SIDE[side], "Unapproved URDF provenance")
|
||||||
_require(manifest["source_urdf"]["mimic"] == MIMIC, "Unexpected L20 mimic equations")
|
_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["root_link"] == "hand_base_link" and len(actual["joints"]) == 21, "Wrong L20 model")
|
||||||
_require(actual["physics_variant"] == "physx", "PhysX variant required")
|
_require(actual["physics_variant"] == "physx", "PhysX variant required")
|
||||||
validate_mimic(Usd.Stage.Open(str(source)), actual)
|
validate_mimic(Usd.Stage.Open(str(source)), actual)
|
||||||
@@ -84,7 +89,8 @@ def bind_source(source, manifest):
|
|||||||
|
|
||||||
def validate_mimic(stage, manifest, passive=False):
|
def validate_mimic(stage, manifest, passive=False):
|
||||||
joints = {j["name"]: j for j in manifest["joints"]}
|
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():
|
for name, joint in joints.items():
|
||||||
prim = stage.GetPrimAtPath(joint["path"])
|
prim = stage.GetPrimAtPath(joint["path"])
|
||||||
_require(not any("PhysxMimic" in s or "Tendon" in s for s in schemas(prim)), "Duplicate/unsupported coupling")
|
_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")
|
_require(joint["axis"] in ("X", "Y", "Z"), "Invalid revolute axis")
|
||||||
if name not in followers:
|
if name not in followers:
|
||||||
continue
|
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)
|
leader, coefficient, offset = newton_mimic(prim)
|
||||||
_require(leader == joints[equation["reference"]]["path"], "Wrong mimic leader")
|
_require(leader == joints[equation["reference"]]["path"], "Wrong mimic leader")
|
||||||
_require(abs(coefficient - equation["multiplier"]) < 1e-6, "Wrong mimic multiplier/sign")
|
_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):
|
def _preserved(original, stage, before):
|
||||||
"""Only leaf world-anchor deactivation, root migration and follower drive suppression are allowed."""
|
"""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"]
|
anchor = before["world_fixed_joints"][0]["path"]
|
||||||
root = before["root_body_path"]
|
root = before["root_body_path"]
|
||||||
source_anchor = original.GetPrimAtPath(anchor)
|
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"
|
source_anchor.IsA(UsdPhysics.FixedJoint) and not source_anchor.GetAllChildren(), "Expected leaf world anchor"
|
||||||
)
|
)
|
||||||
joint = UsdPhysics.Joint(source_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")
|
_require(joint.GetBody1Rel().GetTargets() == [Sdf.Path(root)], "Anchor must attach only to root")
|
||||||
c0, c1 = UsdGeom.XformCache(), UsdGeom.XformCache()
|
c0, c1 = UsdGeom.XformCache(), UsdGeom.XformCache()
|
||||||
# TraverseAll retains the deliberately inactive leaf: no other deletion or
|
# TraverseAll retains the deliberately inactive leaf: no other deletion or
|
||||||
@@ -133,17 +146,22 @@ def _preserved(original, stage, before):
|
|||||||
if path == anchor:
|
if path == anchor:
|
||||||
_require(after.GetAttribute("physics:jointEnabled").Get() is False, "Inactive anchor must remain disabled")
|
_require(after.GetAttribute("physics:jointEnabled").Get() is False, "Inactive anchor must remain disabled")
|
||||||
allowed.add("physics:jointEnabled")
|
allowed.add("physics:jointEnabled")
|
||||||
expected -= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI"}
|
if path == site:
|
||||||
|
expected -= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI", "NewtonArticulationRootAPI"}
|
||||||
if path == root:
|
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 |= {
|
allowed |= {
|
||||||
a.GetName()
|
a.GetName()
|
||||||
for a in stage.GetPrimAtPath(anchor).GetAttributes()
|
for a in stage.GetPrimAtPath(site).GetAttributes()
|
||||||
if a.GetName().startswith("physxArticulation:")
|
if a.GetName().startswith("physxArticulation:") or a.GetName() == "newton:selfCollisionEnabled"
|
||||||
}
|
}
|
||||||
for name in allowed:
|
for name in allowed:
|
||||||
_require(
|
_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",
|
"Root solver property changed",
|
||||||
)
|
)
|
||||||
if path in followers:
|
if path in followers:
|
||||||
@@ -184,13 +202,17 @@ def inspect_prepared(output, source_manifest):
|
|||||||
source = (output.parent / layer.subLayerPaths[0]).resolve(strict=True)
|
source = (output.parent / layer.subLayerPaths[0]).resolve(strict=True)
|
||||||
before = bind_source(source, source_manifest)
|
before = bind_source(source, source_manifest)
|
||||||
_require(layer.customLayerData.get("source_bundle_sha256") == before["asset_sha256"], "Overlay provenance mismatch")
|
_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(
|
_require(
|
||||||
dict(layer.customLayerData.get("schema_context", {})) == schema_context(),
|
dict(layer.customLayerData.get("schema_context", {})) == schema_context(),
|
||||||
"Authoring schema context mismatch; regenerate from source with --schema-plugin-path for both "
|
"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",
|
"installed PhysX and Newton plugins, and use the same plugins when inspecting",
|
||||||
)
|
)
|
||||||
after = inspect(output)
|
after = inspect(output)
|
||||||
|
after["hand_side"] = before["hand_side"]
|
||||||
_require(
|
_require(
|
||||||
not after["world_fixed_joints"] and after["articulation_roots"] == [before["root_body_path"]], "Not floating"
|
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"],
|
"prepared_asset_sha256": after["asset_sha256"],
|
||||||
"source_asset_sha256": before["asset_sha256"],
|
"source_asset_sha256": before["asset_sha256"],
|
||||||
"independent_joint_names": masters,
|
"independent_joint_names": masters,
|
||||||
"mimic": MIMIC,
|
"mimic": model_mimic(before["hand_side"]),
|
||||||
|
"hand_side": before["hand_side"],
|
||||||
"required_physx_version": BACKEND_VERSION,
|
"required_physx_version": BACKEND_VERSION,
|
||||||
"root_body_path": before["root_body_path"],
|
"root_body_path": before["root_body_path"],
|
||||||
"runtime_verified": False,
|
"runtime_verified": False,
|
||||||
@@ -225,7 +248,7 @@ def prepare(source, output, manifest):
|
|||||||
prepare_floating_overlay(source, temporary)
|
prepare_floating_overlay(source, temporary)
|
||||||
stage = Usd.Stage.Open(str(temporary))
|
stage = Usd.Stage.Open(str(temporary))
|
||||||
for joint in before["joints"]:
|
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
|
continue
|
||||||
prim = stage.GetPrimAtPath(joint["path"])
|
prim = stage.GetPrimAtPath(joint["path"])
|
||||||
# Preserve resolved drive defaults as inert attributes when removing API.
|
# Preserve resolved drive defaults as inert attributes when removing API.
|
||||||
@@ -246,7 +269,7 @@ def prepare(source, output, manifest):
|
|||||||
stage.GetRootLayer().customLayerData = {
|
stage.GetRootLayer().customLayerData = {
|
||||||
"purpose": "L20 experimental floating tracking; runtime UNVERIFIED",
|
"purpose": "L20 experimental floating tracking; runtime UNVERIFIED",
|
||||||
"source_bundle_sha256": before["asset_sha256"],
|
"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,
|
"physx_evidence_version": BACKEND_VERSION,
|
||||||
"schema_context": schema_context(),
|
"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 h5py
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from .identity import manifest_side
|
||||||
|
|
||||||
|
|
||||||
class ContractError(ValueError):
|
class ContractError(ValueError):
|
||||||
"""An input cannot be safely interpreted as a reference trajectory."""
|
"""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}")
|
require(name in file.attrs, f"missing root attribute {name}")
|
||||||
attributes[name] = text(file.attrs[name], 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[name] == expected, f"{name}: expected {expected}")
|
||||||
|
require(attributes["hand_side"] in ("left", "right"), "hand_side must be left or right")
|
||||||
require(
|
require(
|
||||||
re.fullmatch(r"[0-9a-f]{64}", attributes["asset_sha256"]) is not None,
|
re.fullmatch(r"[0-9a-f]{64}", attributes["asset_sha256"]) is not None,
|
||||||
"asset_sha256: lowercase SHA-256 required",
|
"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):
|
def validate_against_manifest(data: Demonstrations, manifest: dict):
|
||||||
"""Check identity/order/limits and known URDF mimic equations, not actuator validity."""
|
"""Check identity/order/limits and known URDF mimic equations, not actuator validity."""
|
||||||
require(manifest.get("manifest_version") == "l20_asset_manifest_v1", "unsupported manifest")
|
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["asset_sha256"] == manifest["asset_sha256"], "asset bundle hash mismatch")
|
||||||
require(data.metadata["root_link"] == manifest["root_link"], "root link mismatch")
|
require(data.metadata["root_link"] == manifest["root_link"], "root link mismatch")
|
||||||
joints = manifest["joints"]
|
joints = manifest["joints"]
|
||||||
|
|||||||
@@ -4,6 +4,54 @@ Changelog
|
|||||||
Unreleased
|
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)
|
0.1.1 (2026-09-11)
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,29 @@ class ControlTests(unittest.TestCase):
|
|||||||
with self.assertRaises(ContractError):
|
with self.assertRaises(ContractError):
|
||||||
Limits(force=value)
|
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):
|
def test_reference_envelope_and_invalid_gap(self):
|
||||||
manifest = {
|
manifest = {
|
||||||
"manifest_version": "l20_asset_manifest_v1",
|
"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):
|
class TrackingSchemaContextTests(unittest.TestCase):
|
||||||
def test_registered_schema_preparation_preserves_defaults_and_rejects_context_drift(self):
|
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")
|
installation = os.environ.get("ISAAC_PATH")
|
||||||
if not installation:
|
if not installation:
|
||||||
self.skipTest("ISAAC_PATH required for installed PhysX/Newton CPU schema regression")
|
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")
|
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"
|
newton = installation / "exts/omni.usd.schema.newton/usd/schema/newton/newton_usd_schemas/plugInfo.json"
|
||||||
self.assertTrue(newton.is_file())
|
self.assertTrue(newton.is_file())
|
||||||
manifest = json.loads((ASSET / "tracking_manifest.json").read_text())
|
manifest = json.loads((asset / "tracking_manifest.json").read_text())
|
||||||
source = ASSET / "linkerhand_g20_left/linkerhand_g20_left.usda"
|
source = asset / f"linkerhand_g20_{side}/linkerhand_g20_{side}.usda"
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
directory = Path(directory)
|
directory = Path(directory)
|
||||||
unregistered = directory / "unregistered.usda"
|
unregistered = directory / "unregistered.usda"
|
||||||
@@ -37,7 +42,7 @@ class TrackingSchemaContextTests(unittest.TestCase):
|
|||||||
code = """
|
code = """
|
||||||
import json, sys
|
import json, sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pxr import Usd
|
from pxr import Usd, UsdPhysics
|
||||||
from dex_workbench_tracking.asset import dependencies
|
from dex_workbench_tracking.asset import dependencies
|
||||||
from dex_workbench_tracking.prepared import inspect_prepared, register_schema_plugins
|
from dex_workbench_tracking.prepared import inspect_prepared, register_schema_plugins
|
||||||
source, manifest_path, unregistered, registered, *plugins = sys.argv[1:]
|
source, manifest_path, unregistered, registered, *plugins = sys.argv[1:]
|
||||||
@@ -64,17 +69,24 @@ assert all(result["schema_context"].values())
|
|||||||
assert dependencies(source) == before
|
assert dependencies(source) == before
|
||||||
original = Usd.Stage.Open(source)
|
original = Usd.Stage.Open(source)
|
||||||
stage = Usd.Stage.Open(registered)
|
stage = Usd.Stage.Open(registered)
|
||||||
anchor = manifest["world_fixed_joints"][0]["path"]
|
site = manifest["articulation_roots"][0]
|
||||||
root = stage.GetPrimAtPath(manifest["root_body_path"])
|
root = stage.GetPrimAtPath(manifest["root_body_path"])
|
||||||
for attr in original.GetPrimAtPath(anchor).GetAttributes():
|
roots = [p for p in stage.Traverse() if p.HasAPI(UsdPhysics.ArticulationRootAPI)]
|
||||||
if attr.GetName().startswith("physxArticulation:"):
|
assert roots == [root], roots
|
||||||
for prim in (stage.GetPrimAtPath(anchor), root):
|
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())
|
after = prim.GetAttribute(attr.GetName())
|
||||||
assert after.Get() == attr.Get(), attr.GetPath()
|
assert after.Get() == attr.Get(), attr.GetPath()
|
||||||
assert after.HasAuthoredValue(), attr.GetPath()
|
assert after.HasAuthoredValue(), attr.GetPath()
|
||||||
assert not after.GetTimeSamples(), attr.GetPath()
|
assert not after.GetTimeSamples(), attr.GetPath()
|
||||||
# Registered schema fallbacks do not relax the static/time-sample gate.
|
# 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()
|
stage.GetRootLayer().Save()
|
||||||
try:
|
try:
|
||||||
inspect_prepared(registered, manifest)
|
inspect_prepared(registered, manifest)
|
||||||
@@ -82,7 +94,7 @@ except ValueError as error:
|
|||||||
assert "Time samples prohibited" in str(error), error
|
assert "Time samples prohibited" in str(error), error
|
||||||
else:
|
else:
|
||||||
raise AssertionError("Sampled solver property accepted")
|
raise AssertionError("Sampled solver property accepted")
|
||||||
root.GetAttribute("physxArticulation:sleepThreshold").ClearAtTime(Usd.TimeCode(1))
|
root.GetAttribute(field).ClearAtTime(Usd.TimeCode(1))
|
||||||
stage.GetRootLayer().Save()
|
stage.GetRootLayer().Save()
|
||||||
inspect_prepared(registered, manifest)
|
inspect_prepared(registered, manifest)
|
||||||
print("PASS: isolated registered schema author/inspect, exact default preservation, strict time samples")
|
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",
|
"-c",
|
||||||
code,
|
code,
|
||||||
str(source),
|
str(source),
|
||||||
str(ASSET / "tracking_manifest.json"),
|
str(asset / "tracking_manifest.json"),
|
||||||
str(unregistered),
|
str(unregistered),
|
||||||
str(registered),
|
str(registered),
|
||||||
str(physx[0]),
|
str(physx[0]),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import h5py
|
import h5py
|
||||||
import numpy as np
|
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
|
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:
|
with h5py.File(self.path, "r+") as file:
|
||||||
function(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):
|
def test_load_roundtrip(self):
|
||||||
data = load(self.path, self.manifest)
|
data = load(self.path, self.manifest)
|
||||||
self.assertEqual(data.joint_names, ("finger_a", "finger_b"))
|
self.assertEqual(data.joint_names, ("finger_a", "finger_b"))
|
||||||
@@ -68,7 +130,7 @@ class TrackingTrajectoryTests(unittest.TestCase):
|
|||||||
for name, bad in (
|
for name, bad in (
|
||||||
("schema_version", "v2"),
|
("schema_version", "v2"),
|
||||||
("embodiment", "L30"),
|
("embodiment", "L30"),
|
||||||
("hand_side", "right"),
|
("hand_side", "both"),
|
||||||
("provenance", "unknown"),
|
("provenance", "unknown"),
|
||||||
("asset_sha256", "xyz"),
|
("asset_sha256", "xyz"),
|
||||||
("root_link", ""),
|
("root_link", ""),
|
||||||
@@ -274,7 +336,7 @@ class TrackingTrajectoryTests(unittest.TestCase):
|
|||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
self.assertEqual(json.loads(result.stdout)["asset_compatibility"], "NOT_CHECKED")
|
self.assertEqual(json.loads(result.stdout)["asset_compatibility"], "NOT_CHECKED")
|
||||||
self.edit(lambda file: file.attrs.__setitem__("hand_side", "right"))
|
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.assertNotEqual(result.returncode, 0)
|
||||||
self.assertIn("FAIL", result.stderr)
|
self.assertIn("FAIL", result.stderr)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user