diff --git a/HDF5_REQUIREMENTS.md b/HDF5_REQUIREMENTS.md index c22a412..c9f71d2 100644 --- a/HDF5_REQUIREMENTS.md +++ b/HDF5_REQUIREMENTS.md @@ -6,6 +6,7 @@ **当前用途:浮动腕部与全部手指参考关节的轨迹回放,后续用于学习策略。** +本文第1–9节保持左手交付约定;右手接入差异见第10节,不得直接使用左手哈希、限位或拇指联动。 本文是可独立分享的数据交付说明,与当前仓库校验器一致。它不是通用 HDF5 标准,也不是完整 DexSchema。 请先交付 **1 条短样例**,确认坐标、尺度、关节映射后再批量导出。 @@ -215,3 +216,28 @@ export PYTHONPATH="$PWD/source/dex_workbench${PYTHONPATH:+:$PYTHONPATH}" 不是 HDF5 格式要求。合法的静止或局部手指轨迹不应为了满足诊断而添加伪造动作。 仿真实现与已有验证结果另见 [`L20_TRACKING.md`](L20_TRACKING.md)。本文只约定数据交付,不要求数据方运行 Isaac Sim。 + +## 10. 右手扩展:显式身份,不是左手镜像 + +本次校验器在 `l20_tracking_v1` 中接受 `hand_side="right"`;其余shape、dtype、坐标和有效帧语义不变。 +旧版本校验器仅接受left,读取右数据必须升级本次代码。一个文件不能混合左右手episode。 + +右手必须使用 [`右手 tracking_manifest.json`](assets/robots/dex_hand/linkerhand_g20_right/tracking_manifest.json), +该清单明确 `hand_side=right`,绑定已检查的右URDF身份。旧无side的左清单仍可读,右清单不得省略side。 +左右文件对另一侧清单均拒绝;即使只篡改数据hash/root以匹配另一侧,仍会被side校验拒绝。 + +- `embodiment="L20"`、`root_link="hand_base_link"` 不变;**hand_side改为right**。 +- 右原始转换USD依赖包 `asset_sha256`: + `d38dc5e4d473119b237b86bdd36087788f726abd9f486b1678f4e719d5513098`。 + 不是右prepared覆盖层哈希,不得复制到不同模型数据。 +- 右当前也是21个状态关节;逐项名称与第5节列表一致,但必须仍按**右清单**排序核对,不能假设所有手型如此。 +- 右拇指 `thumb_ip=1.03*thumb_mcp`,不是左手1.02;四指 `dip=.89*pip`,offset均0。 +- 右 `thumb_cmc_pitch/roll/mcp` 上限分别 `.83/1.39/1.25 rad`,四指pip为`1.75 rad`。 + 其他值及浮点精确值按右清单 `lower_rad/upper_rad`;不使用文档舍入值放宽限位。 +- 右URDF `thumb_cmc_yaw`轴为负Z,USD通过局部关节旋转表达。零位/方向必须按右模型重定向, + 不能直接把左轨迹换hand_side/哈希当作右轨迹,也不能简单给全部角度取负。 +- 数据样例仍应来自同版右模型。右手初次接入仅验证合成HDF5;后续一个独立绑定的专家参考 + 已完成80倍慢放回放,见 `L20_TRACKING.md` 第5节;不证明原速或任意专家数据可回放。 + +给右手数据同事发送本文时,附上同版**右清单**与右模型坐标/映射说明。模型来源、转换设置、 +本地使用及发布限制见 [`右手资产说明`](assets/robots/dex_hand/linkerhand_g20_right/README.md)。 diff --git a/L20_TRACKING.md b/L20_TRACKING.md index 85c141e..9159be2 100644 --- a/L20_TRACKING.md +++ b/L20_TRACKING.md @@ -1,5 +1,11 @@ # L20 左手轨迹跟踪:数据交付与准备入口 +本页保留左手验证历史。新增右手入口、实际转换/回放证据与发布限制见 +[`右手资产说明`](assets/robots/dex_hand/linkerhand_g20_right/README.md);左右共用校验器与控制入口,显式绑定hand_side。 +HDF5右手差异见独立契约第10节(右thumb联动1.03,不能套用左1.02)。 +本轮准备门禁新增 `NewtonArticulationRootAPI` 注册上下文;**旧prepared覆盖层需重新生成**。 +左手源资产未改,本轮额外左small两轮480步回归PASS,指标与历史baseline一致。 + ## 本次可用范围与验证状态 提供 **CPU 数据接口、资产清单、浮动控制覆盖层和实验性动力学跟踪入口**,不是完成的 RL 环境。 @@ -254,7 +260,8 @@ PYTHONPATH=source/dex_workbench timeout 300s ~/isaacsim/python.sh \ ### 控制与验收边界 -- 单环境、seed42、240Hz;默认两次各480步,单次允许100–1200步,外部 timeout 300秒。 +- 单环境、seed42、240Hz;默认两次各480步,普通模式单次允许100–1200步,外部 timeout 300秒。 + 显式完整文件模式的有限步数/预算例外见第5节;不会自动扩大默认范围。 只创建手,无物体、桌面、任务注册或训练。不逐步 teleport;仅 reset 写 root/joint 初值并清零速度、 wrench 与目标缓冲、轨迹时间,之后只发 master q_target 和 root COM wrench。 - 世界系 PD 跟踪 link 原点;输出作用在 root COM,补上 `(p_link-p_rootCOM)×F_PD`。 @@ -346,3 +353,96 @@ HDF5用例 RMS 依次为 `0.000646705 m`、`0.006378398 rad`、`0.007675343 rad` 仍有TGS外力迭代、16/21 actuator(5个被动关节)、protobuf重复注册、visualizer/MaterialX与usdrt警告; 未关闭日志或放宽检查。更大范围、负向/多轴腕部运动、接触、真实专家文件、多环境、训练与硬件均未验证。 完整 pre-commit 仍因未安装工具而 NOT_RUN;本次独立审查仍由父会话执行。 + +## 5. 显式扩范围与完整专家参考回放 + +默认工作半径仍为起点周围 **0.1m**,普通模式步数上限仍为 **1200**(默认480)。 +`--workspace-radius 0.8` 仅覆盖工作半径,不改变增益、力/力矩、参考速度、实测安全界限或误差阈值。 +`--full-episode` 必须同时提供 HDF5,允许每轮最多 **32000** 步,并要求所选 episode 的完整末时刻 +与 `steps/240` 一致;拒绝截尾、超时长外推或离网格的完整回放请求。始终两轮、单环境、seed42。 + +CPU `stretch-time` 仅将原始时间戳乘以有限且>=1的系数;保留全部样本、位姿/关节几何路径、有效掩码、 +关节顺序、已应用的坐标变换和来源类别。在说明中追加系数与输入文件 SHA256,不将专家标成合成数据。 +输入仍须严格匹配清单;**没有通用强制替换哈希的选项**。源包身份算法不同的交付必须先独立核对 +原始 URDF 全部 link/joint、仿真依赖和交付说明,并保留原文件及绑定依据,再生成仅本地使用的派生文件。 + +```bash +# BOUND 是已经独立审查并严格绑定右手清单的本地派生文件,不是未经核验的原始交付。 +# EVIDENCE 是仓库外持久目录;输出必须尚不存在。 +MANIFEST=assets/robots/dex_hand/linkerhand_g20_right/tracking_manifest.json +~/isaacsim/python.sh -m dex_workbench_tracking.cli stretch-time "$BOUND" \ + --manifest "$MANIFEST" --factor 80 --output "$EVIDENCE/slow80.hdf5" +~/isaacsim/python.sh -m dex_workbench_tracking.cli validate "$EVIDENCE/slow80.hdf5" --manifest "$MANIFEST" +# 下面是需单独授权600秒资源预算、且确认无其他Kit/GPU任务时的完整实验命令;不是自动重试授权。 +timeout --kill-after=15s 600s ~/isaacsim/python.sh scripts/tracking/track_l20.py \ + assets/robots/dex_hand/linkerhand_g20_right/tracking.usda --manifest "$MANIFEST" \ + --hdf5 "$EVIDENCE/slow80.hdf5" --steps 32000 --full-episode --workspace-radius 0.8 \ + --execute-experimental --headless +``` + +本次指定51帧专家参考原长1.666667秒,统一80倍慢放后完整133.333333秒/轮;不再次应用 +`world_from_source`,不裁剪、重居中或改变关节轨迹。入口只创建手,没有地面/物体,世界Z为负不需要增添场景。 +成功必须同时具备退出0、明确PASS、两轮完整32000步、全部原有动态/重置/位姿与速度重复性门禁; +失败输出记录阶段、已执行步数及可用的最后误差/速度。超时或缺少最终PASS不是成功,不为通过而继续调参。 +慢放结果仅适用于该派生参考,不证明专家原速、接触任务、训练质量或Sim2Real有效。 + +### 恢复后的单次授权完整回放: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就绪声明。 diff --git a/assets/robots/dex_hand/linkerhand_g20_right/README.md b/assets/robots/dex_hand/linkerhand_g20_right/README.md new file mode 100644 index 0000000..5c820d8 --- /dev/null +++ b/assets/robots/dex_hand/linkerhand_g20_right/README.md @@ -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有效性声明。 diff --git a/assets/robots/dex_hand/linkerhand_g20_right/source_provenance.json b/assets/robots/dex_hand/linkerhand_g20_right/source_provenance.json new file mode 100644 index 0000000..e48a4fb --- /dev/null +++ b/assets/robots/dex_hand/linkerhand_g20_right/source_provenance.json @@ -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" +} diff --git a/assets/robots/dex_hand/linkerhand_g20_right/tracking_manifest.json b/assets/robots/dex_hand/linkerhand_g20_right/tracking_manifest.json new file mode 100644 index 0000000..bcc94a6 --- /dev/null +++ b/assets/robots/dex_hand/linkerhand_g20_right/tracking_manifest.json @@ -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." + ] +} diff --git a/scripts/tracking/track_l20.py b/scripts/tracking/track_l20.py index 363638e..48d8516 100644 --- a/scripts/tracking/track_l20.py +++ b/scripts/tracking/track_l20.py @@ -2,38 +2,94 @@ Requires inspected PhysX 110.1.13 + current Isaac Lab ProxyArray/xyzw API. Runtime coupling is tested from passive follower response, not inferred from schema names. -Use an external timeout <=300s. Do not launch alongside another Kit/GPU job. +Use an externally bounded timeout (default budget <=300s; longer runs need approval). +Do not launch alongside another Kit/GPU job. """ import argparse import hashlib import json +import math import random import traceback +from dataclasses import replace from pathlib import Path -def main(): - from isaaclab.app import AppLauncher +def validate_replay_request(steps, full_episode, has_hdf5, workspace_radius): + """Stdlib-only launch bounds, evaluated before Kit or tracking imports.""" + maximum = 32000 if full_episode else 1200 + if not isinstance(steps, int) or not 100 <= steps <= maximum: + raise ValueError(f"Require 100 <= steps <= {maximum}") + if full_episode and not has_hdf5: + raise ValueError("Full-episode mode requires --hdf5") + if workspace_radius is not None and (not math.isfinite(workspace_radius) or workspace_radius <= 0): + raise ValueError("Workspace radius must be finite positive") + +def validate_replay_duration(steps, dt, duration, full_episode): + """Reject extrapolation and partial/off-grid full-episode requests before sampling.""" + if not steps * dt <= duration: + raise ValueError("Reference shorter than requested steps") + if full_episode and abs(steps * dt - duration) > 1e-9: + raise ValueError("Full episode requires exact on-grid end and steps") + + +def configure_presentation(args): + """Resolve opt-in GUI without silently overriding explicit display conflicts.""" + if args.gui: + if args.headless: + raise ValueError("--gui conflicts with --headless") + if (getattr(args, "visualizer_explicit", False) or args.visualizer is not None) and args.visualizer != ["kit"]: + raise ValueError("--gui requires the Kit visualizer; conflicts with --visualizer/--viz") + args.headless, args.visualizer = False, ["kit"] + else: + args.headless = True # Preserve the diagnostic's legacy headless default. + if args.visualizer is None: + args.visualizer = "none" + + +def build_parser(add_app_launcher_args): + """Keep replay argument parsing testable without importing or launching Kit.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("asset", type=Path, help="Experimental prepared overlay, never original fixed preview") parser.add_argument("--manifest", type=Path, required=True, help="Original data/source manifest") parser.add_argument("--hdf5", type=Path, help="Omit for explicitly synthetic diagnostic reference") parser.add_argument("--episode", default="demo_000000") parser.add_argument("--steps", type=int, default=480, help="Per repetition at 240Hz; two repetitions") + parser.add_argument( + "--full-episode", action="store_true", help="Require complete HDF5 duration; allow up to 32000 steps" + ) + parser.add_argument( + "--workspace-radius", type=float, help="Metres from initial root; overrides only workspace_radius" + ) parser.add_argument("--execute-experimental", action="store_true", help="Acknowledge uncalibrated controller") parser.add_argument("--limits", type=Path, help="JSON fields of control.Limits, SI units; UNCALIBRATED") - AppLauncher.add_app_launcher_args(parser) - parser.set_defaults(headless=True, visualizer="none") + parser.add_argument("--gui", action="store_true", help="Opt-in bright-hand/dark-backdrop Kit preview") + add_app_launcher_args(parser) + # None distinguishes omitted flags from explicit --headless / --viz none. + parser.set_defaults(headless=None, visualizer=None) + return parser + + +def main(): + from isaaclab.app import AppLauncher + + parser = build_parser(AppLauncher.add_app_launcher_args) args = parser.parse_args() - if not args.execute_experimental or not 100 <= args.steps <= 1200: - parser.error("Require --execute-experimental and 100 <= steps <= 1200 per repetition") + if not args.execute_experimental: + parser.error("Require --execute-experimental") + try: + configure_presentation(args) + validate_replay_request(args.steps, args.full_episode, args.hdf5 is not None, args.workspace_radius) + except ValueError as error: + parser.error(str(error)) # Kit must choose/register its USD libraries before any pxr-dependent imports. launcher = AppLauncher(args) app = launcher.app exit_code = 0 + failure_context = {"phase": "setup", "completed_repetitions": 0} try: import numpy as np import torch @@ -52,11 +108,13 @@ def main(): manifest = json.loads(args.manifest.read_text()) prepared = inspect_prepared(args.asset, manifest) limits = Limits(**json.loads(args.limits.read_text())) if args.limits else Limits() + if args.workspace_radius is not None: + limits = replace(limits, workspace_radius=args.workspace_radius) data = load(args.hdf5, manifest) if args.hdf5 else synthetic(manifest) episode = data.episodes[args.episode] validate_reference(episode, limits) dt = 1 / 240 - require(args.steps * dt <= episode.time[-1], "Reference shorter than requested steps") + validate_replay_duration(args.steps, dt, episode.time[-1], args.full_episode) reference = sample(episode, np.arange(args.steps + 1) * dt) # Hold-start reset uses zero velocities; require a genuinely moving diagnostic # for every mimic pair, so an ignored/disabled constraint cannot pass at zero. @@ -119,10 +177,25 @@ def main(): anchor_path = "/World/Hand" + manifest["world_fixed_joints"][0]["path"][len(manifest["default_prim"]) :] anchor = sim.stage.GetPrimAtPath(anchor_path) require(anchor.IsValid() and not anchor.IsActive(), "Obsolete world anchor must be inactive") + effective_roots = [p for p in sim.stage.Traverse() if p.HasAPI(UsdPhysics.ArticulationRootAPI)] + require(len(effective_roots) == 1, "Expected exactly one effective articulation root") + require( + str(effective_roots[0].GetPath()) + == "/World/Hand" + manifest["root_body_path"][len(manifest["default_prim"]) :], + "Effective articulation root is not the root body", + ) active_joints = [p for p in sim.stage.Traverse() if p.IsA(UsdPhysics.Joint)] require(len(active_joints) == len(names), "Unexpected active joint count") require(all(p.IsA(UsdPhysics.RevoluteJoint) for p in active_joints), "Unexpected active constraint") + if args.gui: + from dex_workbench_tracking.preview import create_preview + + eye, center = create_preview(sim.stage, episode.wrist_position) + sim.set_camera_view(eye, center) sim.reset() + if args.gui: + sim.render() + print("GUI_READY: cyan line is the wrist reference; bounded physical replay", flush=True) require(not hand.is_fixed_base and hand.num_instances == 1, "Expected one floating PhysX articulation") require( set(hand.joint_names) == set(names) and len(hand.joint_names) == len(names), "Runtime DOF mapping mismatch" @@ -157,6 +230,7 @@ def main(): upper = np.array([j["upper_rad"] for j in manifest["joints"]]) traces, reset_states, summaries = [], [], [] for repetition in range(2): + failure_context = {"phase": "reset", "completed_repetitions": repetition, "repetition": repetition} hand.reset() hand.permanent_wrench_composer.reset() hand.instantaneous_wrench_composer.reset() @@ -184,6 +258,12 @@ def main(): reset_states.append(np.r_[reset_pose, reset_vel, reset_q]) trace, errors = [], [] for step in range(args.steps): + failure_context = { + "phase": "stepping", + "completed_repetitions": repetition, + "repetition": repetition, + "completed_steps_in_repetition": step, + } require(app.is_running(), "Application stopped before finite test completed") pose, velocity, q = state() force, torque = wrench( @@ -206,11 +286,17 @@ def main(): target=tensor(reference.joint_position[step : step + 1, master_columns]), joint_ids=master_ids ) hand.write_data_to_sim() - sim.step(render=False) + sim.step(render=args.gui and step % 8 == 0) hand.update(dt) pose, velocity, q = state() qdot = array(hand.data.joint_vel)[0] + failure_context["completed_steps_in_repetition"] = step + 1 require(all(np.isfinite(v).all() for v in (pose, velocity, q, qdot)), "Nonfinite dynamic state") + failure_context["measured_speeds_m_s_rad_s_rad_s"] = [ + float(np.linalg.norm(velocity[:3])), + float(np.linalg.norm(velocity[3:])), + float(np.max(np.abs(qdot))), + ] require( np.linalg.norm(velocity[:3]) < 0.5 and np.linalg.norm(velocity[3:]) < 3 @@ -229,6 +315,12 @@ def main(): position_error = np.linalg.norm(pose[:3] - reference.wrist_position[step + 1]) angle_error = np.linalg.norm(rotation_error(reference.wrist_quaternion[step + 1], pose[3:])) joint_error = np.max(np.abs(q - reference.joint_position[step + 1])) + failure_context["last_errors_m_rad_rad_rad"] = [ + float(position_error), + float(angle_error), + float(joint_error), + float(residual), + ] require(residual < 0.002, "Mimic runtime residual >0.002rad; parser/constraint not verified") require( position_error < 0.05 and angle_error < 0.5 and joint_error < 0.2, @@ -255,6 +347,7 @@ def main(): "rms_errors_m_rad_rad_rad": np.sqrt((errors**2).mean(axis=0)).tolist(), } ) + failure_context = {"phase": "repeatability", "completed_repetitions": 2, "steps_per_repetition": args.steps} np.testing.assert_allclose(reset_states[0], reset_states[1], atol=1e-6, rtol=0) position_end = 7 + len(names) np.testing.assert_allclose(traces[0][:, :position_end], traces[1][:, :position_end], atol=1e-3, rtol=0) @@ -272,11 +365,17 @@ def main(): "reference_hdf5_sha256": hashlib.sha256(args.hdf5.read_bytes()).hexdigest() if args.hdf5 else None, "reference_description": data.metadata["source_description"], "world_anchor_inactive": not anchor.IsActive(), + "hand_side": data.metadata["hand_side"], + "effective_articulation_root_count": len(effective_roots), "active_state_joint_count": len(active_joints), "runtime_is_fixed_base": hand.is_fixed_base, "seed": 42, "num_envs": 1, "steps_per_repetition": args.steps, + "reference_duration_s": float(episode.time[-1]), + "replayed_duration_s": args.steps * dt, + "reference_coverage": "full" if abs(args.steps * dt - episode.time[-1]) <= 1e-9 else "partial", + "full_episode_requested": args.full_episode, "repetitions": 2, "physx_version": version, "prepared": prepared, @@ -287,10 +386,11 @@ def main(): ), flush=True, ) - except BaseException: + except BaseException as error: # Kit fast shutdown may not return; emit the failure before closing. exit_code = 1 traceback.print_exc() + print(json.dumps({"status": "FAIL", "error": str(error), "progress": failure_context}), flush=True) raise finally: app.close(exit_code=exit_code) diff --git a/source/dex_workbench/config/extension.toml b/source/dex_workbench/config/extension.toml index b1e1f52..d532157 100644 --- a/source/dex_workbench/config/extension.toml +++ b/source/dex_workbench/config/extension.toml @@ -1,7 +1,7 @@ [package] # Semantic Versioning is used: https://semver.org/ -version = "0.1.1" +version = "0.1.2" # Description category = "isaaclab" @@ -42,4 +42,4 @@ name = "dex_workbench.ui_extension_example" # after this extension is installed with ./isaaclab.sh --install. # List package names only; version ranges, extras, and platform markers # come from this extension's setup.py metadata. -# pip_upgrade_dependencies = ["example_package"] \ No newline at end of file +# pip_upgrade_dependencies = ["example_package"] diff --git a/source/dex_workbench/dex_workbench_tracking/asset.py b/source/dex_workbench/dex_workbench_tracking/asset.py index bd708d4..47e51f9 100644 --- a/source/dex_workbench/dex_workbench_tracking/asset.py +++ b/source/dex_workbench/dex_workbench_tracking/asset.py @@ -52,7 +52,26 @@ def dependencies(source): return [{"path": name, "sha256": sha} for name, sha in entries], digest.hexdigest() -def inspect(source, source_urdf=None): +def require_static_endpoint(stage, path): + """Only an identity default Xform can stand for the importer's static world.""" + prim = stage.GetPrimAtPath(path) + _require(prim == stage.GetDefaultPrim() and prim.IsA(UsdGeom.Xform), "Unsupported static anchor endpoint") + while prim and not prim.IsPseudoRoot(): + _require( + not prim.HasAPI(UsdPhysics.RigidBodyAPI) and not prim.HasAPI(UsdPhysics.ArticulationRootAPI), + "Static endpoint has rigid/articulation API", + ) + _require(not any(a.GetTimeSamples() for a in prim.GetAttributes()), "Sampled static endpoint") + xform = UsdGeom.Xformable(prim) + _require(not xform.GetResetXformStack(), "Static endpoint reset stack prohibited") + _require( + Gf.IsClose(UsdGeom.XformCache().GetLocalToWorldTransform(prim), Gf.Matrix4d(1), 1e-12), + "Static endpoint must have identity world transform", + ) + prim = prim.GetParent() + + +def inspect(source, source_urdf=None, hand_side=None): source = Path(source).resolve(strict=True) deps, bundle_sha = dependencies(source) stage = Usd.Stage.Open(str(source)) @@ -86,7 +105,9 @@ def inspect(source, source_urdf=None): b0 = [str(p) for p in joint.GetBody0Rel().GetTargets()] b1 = [str(p) for p in joint.GetBody1Rel().GetTargets()] _require(len(b1) == 1 and b1[0] in body_paths, f"Unresolved/non-body body1: {prim.GetPath()}") - if not b0: + if not b0 or (len(b0) == 1 and b0[0] not in body_paths): + if b0: + require_static_endpoint(stage, b0[0]) _require(prim.IsA(UsdPhysics.FixedJoint), "Unsupported non-fixed world joint") world_joints.append({"path": str(prim.GetPath()), "body1": b1[0]}) continue @@ -187,7 +208,14 @@ def inspect(source, source_urdf=None): coupling_status = "MIMIC_SCHEMA_PRESENT_RUNTIME_UNVERIFIED" elif source_info["mimic"]: coupling_status = "MISSING_MIMIC_SCHEMA" + identity = {} + if hand_side is not None: + from .identity import manifest_side + + identity = {"hand_side": hand_side} + manifest_side(dict(identity, source_urdf=source_info)) return { + **identity, "manifest_version": "l20_asset_manifest_v1", "asset_sha256": bundle_sha, "entry_file": source.name, @@ -238,9 +266,11 @@ def prepare_floating_overlay(source, output): before = inspect(source) _require(before["physics_variant"] == "physx", "Only the current PhysX variant is supported") anchors = before["world_fixed_joints"] + _require(len(anchors) == 1, "Expected one fixed-world anchor") + site = before["articulation_roots"][0] _require( - len(anchors) == 1 and before["articulation_roots"] == [anchors[0]["path"]], - "Expected a fixed-world articulation root; will not guess topology", + site in (anchors[0]["path"], before["root_body_path"].rsplit("/", 1)[0]), + "Expected fixed anchor or exact root container articulation site", ) original = Usd.Stage.Open(str(source)) stage = Usd.Stage.CreateInMemory() @@ -252,19 +282,28 @@ def prepare_floating_overlay(source, output): anchor = stage.GetPrimAtPath(anchors[0]["path"]) # Snapshot resolved schema fallbacks before removing the API. Preserve them # explicitly on the inert anchor and the new root, not just authored values. + articulation_site = stage.GetPrimAtPath(site) articulation_values = [] - for attr in anchor.GetAttributes(): - if attr.GetName().startswith("physxArticulation:") and attr.Get() is not None: + for attr in articulation_site.GetAttributes(): + if ( + attr.GetName().startswith("physxArticulation:") or attr.GetName() == "newton:selfCollisionEnabled" + ) and attr.Get() is not None: _require(not attr.GetTimeSamples(), f"Time samples prohibited: {attr.GetPath()}") articulation_values.append((attr.GetName(), attr.GetTypeName(), attr.Get())) - anchor.RemoveAPI(UsdPhysics.ArticulationRootAPI) - anchor.RemoveAppliedSchema("PhysxArticulationAPI") + source_schemas = articulation_site.GetMetadata("apiSchemas").GetAppliedItems() + articulation_site.RemoveAPI(UsdPhysics.ArticulationRootAPI) + articulation_site.RemoveAppliedSchema("PhysxArticulationAPI") + if "NewtonArticulationRootAPI" in source_schemas: + articulation_site.RemoveAppliedSchema("NewtonArticulationRootAPI") UsdPhysics.Joint(anchor).GetJointEnabledAttr().Set(False) root = stage.GetPrimAtPath(before["root_body_path"]) UsdPhysics.ArticulationRootAPI.Apply(root) - root.AddAppliedSchema("PhysxArticulationAPI") + if "PhysxArticulationAPI" in source_schemas: + root.AddAppliedSchema("PhysxArticulationAPI") + if "NewtonArticulationRootAPI" in source_schemas: + root.AddAppliedSchema("NewtonArticulationRootAPI") for name, dtype, value in articulation_values: - anchor.CreateAttribute(name, dtype).Set(value) + articulation_site.CreateAttribute(name, dtype).Set(value) root.CreateAttribute(name, dtype).Set(value) stage.GetRootLayer().customLayerData = {"purpose": "L20 topology-only diagnostic; dynamic replay BLOCKED"} cache0, cache1 = UsdGeom.XformCache(), UsdGeom.XformCache() @@ -305,15 +344,18 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("asset", type=Path) parser.add_argument("--source-urdf", type=Path, help="Optional local original; inspected, never modified") + parser.add_argument( + "--hand-side", choices=("left", "right"), help="Explicit inspected identity; right requires source URDF" + ) parser.add_argument("--output", type=Path, required=True, help="New JSON manifest; no overwrite") parser.add_argument("--floating-overlay", type=Path, help="New topology-only USD layer; no overwrite") args = parser.parse_args() try: _require(not args.output.exists(), "Refusing to overwrite manifest") - manifest = inspect(args.asset, args.source_urdf) + manifest = inspect(args.asset, args.source_urdf, args.hand_side) if args.floating_overlay: prepare_floating_overlay(args.asset, args.floating_overlay) - manifest = inspect(args.floating_overlay, args.source_urdf) + manifest = inspect(args.floating_overlay, args.source_urdf, args.hand_side) args.output.parent.mkdir(parents=True, exist_ok=True) with args.output.open("x", encoding="utf-8") as stream: json.dump(manifest, stream, indent=2, allow_nan=False) diff --git a/source/dex_workbench/dex_workbench_tracking/cli.py b/source/dex_workbench/dex_workbench_tracking/cli.py index 64c3ce7..1d11489 100644 --- a/source/dex_workbench/dex_workbench_tracking/cli.py +++ b/source/dex_workbench/dex_workbench_tracking/cli.py @@ -1,6 +1,7 @@ """CPU preparation CLI: validate, synthesize test references, or resample HDF5.""" import argparse +import hashlib import json import os import tempfile @@ -9,6 +10,7 @@ from pathlib import Path import h5py import numpy as np +from .identity import manifest_side from .trajectory import ( ContractError, Demonstrations, @@ -48,6 +50,26 @@ def publish_validated(path, data, manifest): os.link(temporary, path) +def stretch_time(data, factor): + """Slow references without altering any pose, joint, valid mask or coordinate metadata.""" + require(np.isfinite(factor) and factor >= 1, "Time stretch factor must be finite and >= 1") + episodes = {} + for name, episode in data.episodes.items(): + with np.errstate(over="ignore"): + time = episode.time * factor + require(np.isfinite(time).all(), "Time stretch overflow") + episodes[name] = Episode( + time, + episode.wrist_position.copy(), + episode.wrist_quaternion.copy(), + episode.joint_position.copy(), + episode.valid.copy(), + ) + metadata = dict(data.metadata) + metadata["source_description"] += f"; uniform time stretch factor={factor:g}; all original samples preserved" + return Demonstrations(metadata, data.joint_names, data.world_from_source.copy(), episodes) + + def synthetic(manifest, profile="small"): """Named analytic fixtures, not experts. ``range_4s`` has smooth out-and-back endpoints.""" require(profile in ("small", "range_4s"), "Unknown synthetic profile") @@ -81,7 +103,7 @@ def synthetic(manifest, profile="small"): metadata = { "schema_version": "l20_tracking_v1", "embodiment": "L20", - "hand_side": "left", + "hand_side": manifest_side(manifest), "asset_sha256": manifest["asset_sha256"], "root_link": manifest["root_link"], "metric_scale_provenance": "Analytic SI fixture, not video reconstruction or measurement", @@ -124,6 +146,13 @@ def main(): resample.add_argument("--manifest", required=True, type=Path) resample.add_argument("--hz", required=True, type=float) resample.add_argument("--output", required=True, type=Path) + stretch = commands.add_parser( + "stretch-time", help="Uniform slowdown; no pose/coordinate changes or identity override" + ) + stretch.add_argument("input", type=Path) + stretch.add_argument("--manifest", required=True, type=Path) + stretch.add_argument("--factor", required=True, type=float) + stretch.add_argument("--output", required=True, type=Path) gate = commands.add_parser("replay-check", help="Fails closed until a physical tracking Adapter is implemented") gate.add_argument("--manifest", required=True, type=Path) args = parser.parse_args() @@ -139,6 +168,12 @@ def main(): publish_validated(args.output, data, manifest) else: data = load(args.input, manifest) + if args.command == "stretch-time": + data = stretch_time(data, args.factor) + data.metadata["source_description"] += ( + f"; stretch input HDF5 SHA256={hashlib.sha256(args.input.read_bytes()).hexdigest()}" + ) + publish_validated(args.output, data, manifest) if args.command == "resample": require(np.isfinite(args.hz) and 0 < args.hz <= 1000, "hz must be finite and in (0, 1000]") episodes = {} diff --git a/source/dex_workbench/dex_workbench_tracking/identity.py b/source/dex_workbench/dex_workbench_tracking/identity.py new file mode 100644 index 0000000..ed271cc --- /dev/null +++ b/source/dex_workbench/dex_workbench_tracking/identity.py @@ -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, + } + ] diff --git a/source/dex_workbench/dex_workbench_tracking/prepared.py b/source/dex_workbench/dex_workbench_tracking/prepared.py index 9256d90..1d513b8 100644 --- a/source/dex_workbench/dex_workbench_tracking/prepared.py +++ b/source/dex_workbench/dex_workbench_tracking/prepared.py @@ -12,14 +12,12 @@ from pathlib import Path from pxr import Gf, Plug, Sdf, Usd, UsdGeom, UsdPhysics -from .asset import _require, dependencies, inspect, prepare_floating_overlay +from .asset import _require, dependencies, inspect, prepare_floating_overlay, require_static_endpoint +from .identity import LEFT_URDF_SHA, SOURCE_SHA_BY_SIDE, manifest_side, model_mimic -SOURCE_URDF_SHA = "b8ef22e436ab311fb61f87091b72ae717821092471d628d0ea9475b5088daa5d" +SOURCE_URDF_SHA = LEFT_URDF_SHA BACKEND_VERSION = "110.1.13" -MIMIC = [ - {"joint": f"{finger}_dip", "reference": f"{finger}_pip", "multiplier": 0.89, "offset_rad": 0.0} - for finger in ("index", "middle", "pinky", "ring") -] + [{"joint": "thumb_ip", "reference": "thumb_mcp", "multiplier": 1.02, "offset_rad": 0.0}] +MIMIC = model_mimic("left") # Existing left callers retain the same constant. DRIVE_FIELDS = ("stiffness", "damping", "maxForce") @@ -33,7 +31,8 @@ def schema_context(): """Resolved schema fallbacks are part of the authoring/inspection contract.""" registry = Usd.SchemaRegistry() return { - name: bool(registry.FindAppliedAPIPrimDefinition(name)) for name in ("PhysxArticulationAPI", "NewtonMimicAPI") + name: bool(registry.FindAppliedAPIPrimDefinition(name)) + for name in ("PhysxArticulationAPI", "NewtonMimicAPI", "NewtonArticulationRootAPI") } @@ -71,11 +70,17 @@ def newton_mimic(prim): def bind_source(source, manifest): """Reject stale identity/structural metadata before authoring or accepting an overlay.""" + side = manifest_side(manifest) actual = inspect(source) + actual["hand_side"] = side for key in ("asset_sha256", "joints", "root_body_path", "root_link", "dependencies", "bodies"): _require(actual[key] == manifest[key], f"Stale/inconsistent source manifest: {key}") - _require(manifest["source_urdf"]["sha256"] == SOURCE_URDF_SHA, "Unapproved URDF provenance") - _require(manifest["source_urdf"]["mimic"] == MIMIC, "Unexpected L20 mimic equations") + _require(manifest["source_urdf"]["sha256"] == SOURCE_SHA_BY_SIDE[side], "Unapproved URDF provenance") + _require(manifest["source_urdf"]["mimic"] == model_mimic(side), "Unexpected L20 mimic equations") + anchors = actual["world_fixed_joints"] + _require(len(anchors) == 1, "Expected one source world anchor") + expected_site = anchors[0]["path"] if side == "left" else actual["default_prim"] + "/Geometry" + _require(actual["articulation_roots"] == [expected_site], "Unexpected source articulation container") _require(actual["root_link"] == "hand_base_link" and len(actual["joints"]) == 21, "Wrong L20 model") _require(actual["physics_variant"] == "physx", "PhysX variant required") validate_mimic(Usd.Stage.Open(str(source)), actual) @@ -84,7 +89,8 @@ def bind_source(source, manifest): def validate_mimic(stage, manifest, passive=False): joints = {j["name"]: j for j in manifest["joints"]} - followers = {eq["joint"] for eq in MIMIC} + equations = model_mimic(manifest.get("hand_side", "left")) + followers = {eq["joint"] for eq in equations} for name, joint in joints.items(): prim = stage.GetPrimAtPath(joint["path"]) _require(not any("PhysxMimic" in s or "Tendon" in s for s in schemas(prim)), "Duplicate/unsupported coupling") @@ -93,7 +99,7 @@ def validate_mimic(stage, manifest, passive=False): _require(joint["axis"] in ("X", "Y", "Z"), "Invalid revolute axis") if name not in followers: continue - equation = next(eq for eq in MIMIC if eq["joint"] == name) + equation = next(eq for eq in equations if eq["joint"] == name) leader, coefficient, offset = newton_mimic(prim) _require(leader == joints[equation["reference"]]["path"], "Wrong mimic leader") _require(abs(coefficient - equation["multiplier"]) < 1e-6, "Wrong mimic multiplier/sign") @@ -107,7 +113,10 @@ def validate_mimic(stage, manifest, passive=False): def _preserved(original, stage, before): """Only leaf world-anchor deactivation, root migration and follower drive suppression are allowed.""" - followers = {j["path"] for j in before["joints"] if j["name"] in {eq["joint"] for eq in MIMIC}} + followers = { + j["path"] for j in before["joints"] if j["name"] in {eq["joint"] for eq in model_mimic(before["hand_side"])} + } + site = before["articulation_roots"][0] anchor = before["world_fixed_joints"][0]["path"] root = before["root_body_path"] source_anchor = original.GetPrimAtPath(anchor) @@ -115,7 +124,11 @@ def _preserved(original, stage, before): source_anchor.IsA(UsdPhysics.FixedJoint) and not source_anchor.GetAllChildren(), "Expected leaf world anchor" ) joint = UsdPhysics.Joint(source_anchor) - _require(not joint.GetBody0Rel().GetTargets(), "Anchor must attach directly to world") + targets = joint.GetBody0Rel().GetTargets() + if targets: + _require(len(targets) == 1 and before["hand_side"] == "right", "Unexpected anchor endpoint") + require_static_endpoint(original, targets[0]) + require_static_endpoint(stage, targets[0]) _require(joint.GetBody1Rel().GetTargets() == [Sdf.Path(root)], "Anchor must attach only to root") c0, c1 = UsdGeom.XformCache(), UsdGeom.XformCache() # TraverseAll retains the deliberately inactive leaf: no other deletion or @@ -133,17 +146,22 @@ def _preserved(original, stage, before): if path == anchor: _require(after.GetAttribute("physics:jointEnabled").Get() is False, "Inactive anchor must remain disabled") allowed.add("physics:jointEnabled") - expected -= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI"} + if path == site: + expected -= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI", "NewtonArticulationRootAPI"} if path == root: - expected |= {"PhysicsArticulationRootAPI", "PhysxArticulationAPI"} + expected.add("PhysicsArticulationRootAPI") + if "PhysxArticulationAPI" in schemas(original.GetPrimAtPath(site)): + expected.add("PhysxArticulationAPI") + if "NewtonArticulationRootAPI" in schemas(original.GetPrimAtPath(site)): + expected.add("NewtonArticulationRootAPI") allowed |= { a.GetName() - for a in stage.GetPrimAtPath(anchor).GetAttributes() - if a.GetName().startswith("physxArticulation:") + for a in stage.GetPrimAtPath(site).GetAttributes() + if a.GetName().startswith("physxArticulation:") or a.GetName() == "newton:selfCollisionEnabled" } for name in allowed: _require( - after.GetAttribute(name).Get() == stage.GetPrimAtPath(anchor).GetAttribute(name).Get(), + after.GetAttribute(name).Get() == stage.GetPrimAtPath(site).GetAttribute(name).Get(), "Root solver property changed", ) if path in followers: @@ -184,13 +202,17 @@ def inspect_prepared(output, source_manifest): source = (output.parent / layer.subLayerPaths[0]).resolve(strict=True) before = bind_source(source, source_manifest) _require(layer.customLayerData.get("source_bundle_sha256") == before["asset_sha256"], "Overlay provenance mismatch") - _require(layer.customLayerData.get("source_urdf_sha256") == SOURCE_URDF_SHA, "Overlay URDF mismatch") + _require( + layer.customLayerData.get("source_urdf_sha256") == SOURCE_SHA_BY_SIDE[before["hand_side"]], + "Overlay URDF mismatch", + ) _require( dict(layer.customLayerData.get("schema_context", {})) == schema_context(), "Authoring schema context mismatch; regenerate from source with --schema-plugin-path for both " "installed PhysX and Newton plugins, and use the same plugins when inspecting", ) after = inspect(output) + after["hand_side"] = before["hand_side"] _require( not after["world_fixed_joints"] and after["articulation_roots"] == [before["root_body_path"]], "Not floating" ) @@ -208,7 +230,8 @@ def inspect_prepared(output, source_manifest): "prepared_asset_sha256": after["asset_sha256"], "source_asset_sha256": before["asset_sha256"], "independent_joint_names": masters, - "mimic": MIMIC, + "mimic": model_mimic(before["hand_side"]), + "hand_side": before["hand_side"], "required_physx_version": BACKEND_VERSION, "root_body_path": before["root_body_path"], "runtime_verified": False, @@ -225,7 +248,7 @@ def prepare(source, output, manifest): prepare_floating_overlay(source, temporary) stage = Usd.Stage.Open(str(temporary)) for joint in before["joints"]: - if joint["name"] not in {eq["joint"] for eq in MIMIC}: + if joint["name"] not in {eq["joint"] for eq in model_mimic(before["hand_side"])}: continue prim = stage.GetPrimAtPath(joint["path"]) # Preserve resolved drive defaults as inert attributes when removing API. @@ -246,7 +269,7 @@ def prepare(source, output, manifest): stage.GetRootLayer().customLayerData = { "purpose": "L20 experimental floating tracking; runtime UNVERIFIED", "source_bundle_sha256": before["asset_sha256"], - "source_urdf_sha256": SOURCE_URDF_SHA, + "source_urdf_sha256": SOURCE_SHA_BY_SIDE[before["hand_side"]], "physx_evidence_version": BACKEND_VERSION, "schema_context": schema_context(), } diff --git a/source/dex_workbench/dex_workbench_tracking/preview.py b/source/dex_workbench/dex_workbench_tracking/preview.py new file mode 100644 index 0000000..7f1de9a --- /dev/null +++ b/source/dex_workbench/dex_workbench_tracking/preview.py @@ -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) diff --git a/source/dex_workbench/dex_workbench_tracking/trajectory.py b/source/dex_workbench/dex_workbench_tracking/trajectory.py index 9c676df..2738630 100644 --- a/source/dex_workbench/dex_workbench_tracking/trajectory.py +++ b/source/dex_workbench/dex_workbench_tracking/trajectory.py @@ -7,6 +7,8 @@ from pathlib import Path import h5py import numpy as np +from .identity import manifest_side + class ContractError(ValueError): """An input cannot be safely interpreted as a reference trajectory.""" @@ -70,8 +72,9 @@ def load(path: str | Path, manifest: dict | None = None) -> Demonstrations: ): require(name in file.attrs, f"missing root attribute {name}") attributes[name] = text(file.attrs[name], name) - for name, expected in (("schema_version", "l20_tracking_v1"), ("embodiment", "L20"), ("hand_side", "left")): + for name, expected in (("schema_version", "l20_tracking_v1"), ("embodiment", "L20")): require(attributes[name] == expected, f"{name}: expected {expected}") + require(attributes["hand_side"] in ("left", "right"), "hand_side must be left or right") require( re.fullmatch(r"[0-9a-f]{64}", attributes["asset_sha256"]) is not None, "asset_sha256: lowercase SHA-256 required", @@ -150,6 +153,7 @@ def load(path: str | Path, manifest: dict | None = None) -> Demonstrations: def validate_against_manifest(data: Demonstrations, manifest: dict): """Check identity/order/limits and known URDF mimic equations, not actuator validity.""" require(manifest.get("manifest_version") == "l20_asset_manifest_v1", "unsupported manifest") + require(data.metadata["hand_side"] == manifest_side(manifest), "hand_side mismatch") require(data.metadata["asset_sha256"] == manifest["asset_sha256"], "asset bundle hash mismatch") require(data.metadata["root_link"] == manifest["root_link"], "root link mismatch") joints = manifest["joints"] diff --git a/source/dex_workbench/docs/CHANGELOG.rst b/source/dex_workbench/docs/CHANGELOG.rst index 463ac55..b449b6e 100644 --- a/source/dex_workbench/docs/CHANGELOG.rst +++ b/source/dex_workbench/docs/CHANGELOG.rst @@ -4,6 +4,54 @@ Changelog Unreleased ~~~~~~~~~~ +0.1.2 (2026-09-14) +~~~~~~~~~~~~~~~~~~ + +Local bilateral tracking and bounded expert-reference milestone, not full release +clearance. Right USDs and expert data are local-only and NOT bundled: clean-clone +right-asset tests/replay remain blocked pending storage/licensing resolution. +Full pre-commit is unavailable; consolidated GUI E2E has not been run. +Cartpole and its task ID are unchanged; older prepared overlays need regeneration. + +Added +^^^^^ + +* Add opt-in ``--gui`` to the existing replay runner, with the approved bright-hand + lighting, cyan wrist path, fixed camera and dark non-physical backdrop in a small + presentation helper. Keep default headless execution and 240Hz controls unchanged; + render every eight physics steps only in GUI mode. Add CPU display/flag regressions. + The separate GUI clone was visually approved; this consolidated CLI is NOT_RUN in Kit. +* Add explicit workspace-radius override and finite full-episode HDF5 replay + (at most 32000 steps per repetition), retaining the default 0.1m/1200-step bounds + and all controller/safety thresholds. Add strict duration and preservation tests. +* Add identity-validated, no-overwrite ``stretch-time`` preparation that preserves + original samples, geometry, coordinate metadata and expert provenance; record + the factor and input SHA256. No generic asset-hash override or original-speed claim. +* Complete one separately authorized 600s-budget recovery replay: right expert + reference uniformly slowed 80x, full 2x32000 steps at 240Hz, exit0/PASS in 384s; + 81 CPU regressions pass. Preserve the interrupted run as unknown and optional + interpolated-grid equality audit failures; no runtime safety thresholds relaxed. +* Convert the user-provided L20 RIGHT URDF with the specified Isaac Lab converter; + record source provenance and right manifest for its self-contained local USD bundle + (generated USD files are excluded from this commit). + Geometry exceeds the ordinary-Git 2000KB gate: publication remains blocked + pending approved LFS/external storage and redistribution licensing confirmation. +* Extend the existing tracking contract with explicit right-side identity and its + inspected thumb mimic coefficient 1.03; preserve legacy left data/manifest support. + Add cross-side, axis, relocation and strict importer-topology regressions. +* Validate right small 2x480 and synthetic real-file HDF5 2x960 steps, plus left + small 2x480 regression, with unchanged controls and assertions. Those earlier + synthetic runs are not expert-data evidence; the separate slowed expert run is above. + +Fixed +^^^^^ + +* Handle the exact right importer static default-Xform anchor and Geometry root + site. Migrate source-present Newton articulation API too, because its registered + schema inherits PhysicsArticulationRootAPI; preserve resolved source properties. + Check exactly one effective runtime root. Schema context now includes Newton + articulation registration; older prepared overlays must be regenerated. + 0.1.1 (2026-09-11) ~~~~~~~~~~~~~~~~~~ diff --git a/source/dex_workbench/tests/test_tracking_control.py b/source/dex_workbench/tests/test_tracking_control.py index 22f1900..c530a38 100644 --- a/source/dex_workbench/tests/test_tracking_control.py +++ b/source/dex_workbench/tests/test_tracking_control.py @@ -81,6 +81,29 @@ class ControlTests(unittest.TestCase): with self.assertRaises(ContractError): Limits(force=value) + def test_workspace_extension_does_not_relax_velocity_or_forces(self): + from dataclasses import replace + + manifest = { + "manifest_version": "l20_asset_manifest_v1", + "asset_sha256": "a" * 64, + "root_link": "test", + "joints": [{"name": "a", "lower_rad": 0, "upper_rad": 1}], + } + episode = synthetic(manifest).episodes["demo_000000"] + episode.time[:] *= 80 + episode.wrist_position[:, 0] *= 300 + with self.assertRaisesRegex(ContractError, "outside workspace"): + validate_reference(episode, self.limits) + extended = replace(self.limits, workspace_radius=0.8) + validate_reference(episode, extended) + for key, value in vars(self.limits).items(): + if key != "workspace_radius": + self.assertEqual(getattr(extended, key), value) + episode.time[:] /= 80 + with self.assertRaisesRegex(ContractError, "too fast"): + validate_reference(episode, extended) + def test_reference_envelope_and_invalid_gap(self): manifest = { "manifest_version": "l20_asset_manifest_v1", diff --git a/source/dex_workbench/tests/test_tracking_preview.py b/source/dex_workbench/tests/test_tracking_preview.py new file mode 100644 index 0000000..1887322 --- /dev/null +++ b/source/dex_workbench/tests/test_tracking_preview.py @@ -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) diff --git a/source/dex_workbench/tests/test_tracking_replay.py b/source/dex_workbench/tests/test_tracking_replay.py new file mode 100644 index 0000000..2ebf2ea --- /dev/null +++ b/source/dex_workbench/tests/test_tracking_replay.py @@ -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) diff --git a/source/dex_workbench/tests/test_tracking_right.py b/source/dex_workbench/tests/test_tracking_right.py new file mode 100644 index 0000000..b1f9c26 --- /dev/null +++ b/source/dex_workbench/tests/test_tracking_right.py @@ -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() diff --git a/source/dex_workbench/tests/test_tracking_schema_context.py b/source/dex_workbench/tests/test_tracking_schema_context.py index 2fbd747..3454d9c 100644 --- a/source/dex_workbench/tests/test_tracking_schema_context.py +++ b/source/dex_workbench/tests/test_tracking_schema_context.py @@ -16,6 +16,11 @@ ASSET = REPO / "assets/robots/dex_hand/linkerhand_g20_left" class TrackingSchemaContextTests(unittest.TestCase): def test_registered_schema_preparation_preserves_defaults_and_rejects_context_drift(self): + for side in ("left", "right"): + with self.subTest(side=side): + self.check_registered(REPO / f"assets/robots/dex_hand/linkerhand_g20_{side}", side) + + def check_registered(self, asset, side): installation = os.environ.get("ISAAC_PATH") if not installation: self.skipTest("ISAAC_PATH required for installed PhysX/Newton CPU schema regression") @@ -24,8 +29,8 @@ class TrackingSchemaContextTests(unittest.TestCase): self.assertEqual(len(physx), 1, "Select an unambiguous installed PhysX schema for this test") newton = installation / "exts/omni.usd.schema.newton/usd/schema/newton/newton_usd_schemas/plugInfo.json" self.assertTrue(newton.is_file()) - manifest = json.loads((ASSET / "tracking_manifest.json").read_text()) - source = ASSET / "linkerhand_g20_left/linkerhand_g20_left.usda" + manifest = json.loads((asset / "tracking_manifest.json").read_text()) + source = asset / f"linkerhand_g20_{side}/linkerhand_g20_{side}.usda" with tempfile.TemporaryDirectory() as directory: directory = Path(directory) unregistered = directory / "unregistered.usda" @@ -37,7 +42,7 @@ class TrackingSchemaContextTests(unittest.TestCase): code = """ import json, sys from pathlib import Path -from pxr import Usd +from pxr import Usd, UsdPhysics from dex_workbench_tracking.asset import dependencies from dex_workbench_tracking.prepared import inspect_prepared, register_schema_plugins source, manifest_path, unregistered, registered, *plugins = sys.argv[1:] @@ -64,17 +69,24 @@ assert all(result["schema_context"].values()) assert dependencies(source) == before original = Usd.Stage.Open(source) stage = Usd.Stage.Open(registered) -anchor = manifest["world_fixed_joints"][0]["path"] +site = manifest["articulation_roots"][0] root = stage.GetPrimAtPath(manifest["root_body_path"]) -for attr in original.GetPrimAtPath(anchor).GetAttributes(): - if attr.GetName().startswith("physxArticulation:"): - for prim in (stage.GetPrimAtPath(anchor), root): +roots = [p for p in stage.Traverse() if p.HasAPI(UsdPhysics.ArticulationRootAPI)] +assert roots == [root], roots +if manifest.get("hand_side") == "right": + assert not stage.GetPrimAtPath(site).HasAPI(UsdPhysics.ArticulationRootAPI) + assert "NewtonArticulationRootAPI" not in stage.GetPrimAtPath(site).GetAppliedSchemas() + assert "NewtonArticulationRootAPI" in root.GetAppliedSchemas() +for attr in original.GetPrimAtPath(site).GetAttributes(): + if attr.GetName().startswith("physxArticulation:") or attr.GetName() == "newton:selfCollisionEnabled": + for prim in (stage.GetPrimAtPath(site), root): after = prim.GetAttribute(attr.GetName()) assert after.Get() == attr.Get(), attr.GetPath() assert after.HasAuthoredValue(), attr.GetPath() assert not after.GetTimeSamples(), attr.GetPath() # Registered schema fallbacks do not relax the static/time-sample gate. -root.GetAttribute("physxArticulation:sleepThreshold").Set(99, Usd.TimeCode(1)) +field = "newton:selfCollisionEnabled" if manifest.get("hand_side") == "right" else "physxArticulation:sleepThreshold" +root.GetAttribute(field).Set(True if manifest.get("hand_side") == "right" else 99, Usd.TimeCode(1)) stage.GetRootLayer().Save() try: inspect_prepared(registered, manifest) @@ -82,7 +94,7 @@ except ValueError as error: assert "Time samples prohibited" in str(error), error else: raise AssertionError("Sampled solver property accepted") -root.GetAttribute("physxArticulation:sleepThreshold").ClearAtTime(Usd.TimeCode(1)) +root.GetAttribute(field).ClearAtTime(Usd.TimeCode(1)) stage.GetRootLayer().Save() inspect_prepared(registered, manifest) print("PASS: isolated registered schema author/inspect, exact default preservation, strict time samples") @@ -93,7 +105,7 @@ print("PASS: isolated registered schema author/inspect, exact default preservati "-c", code, str(source), - str(ASSET / "tracking_manifest.json"), + str(asset / "tracking_manifest.json"), str(unregistered), str(registered), str(physx[0]), diff --git a/source/dex_workbench/tests/test_tracking_trajectory.py b/source/dex_workbench/tests/test_tracking_trajectory.py index e4684ed..1c7967d 100644 --- a/source/dex_workbench/tests/test_tracking_trajectory.py +++ b/source/dex_workbench/tests/test_tracking_trajectory.py @@ -11,7 +11,7 @@ from pathlib import Path import h5py import numpy as np -from dex_workbench_tracking.cli import publish_validated, synthetic, write +from dex_workbench_tracking.cli import publish_validated, stretch_time, synthetic, write from dex_workbench_tracking.trajectory import ContractError, load, require_dynamic_replay_ready, sample @@ -38,6 +38,68 @@ class TrackingTrajectoryTests(unittest.TestCase): with h5py.File(self.path, "r+") as file: function(file) + def test_stretch_time_preserves_samples_identity_and_input(self): + original = load(self.path, self.manifest) + original.metadata["provenance"] = "expert_retargeted" + original.world_from_source[:3, :3] = [[1, 0, 0], [0, 0, 1], [0, -1, 0]] + source_bytes = self.path.read_bytes() + result = stretch_time(original, 80) + self.assertEqual(result.metadata["provenance"], "expert_retargeted") + self.assertEqual(result.metadata["asset_sha256"], original.metadata["asset_sha256"]) + self.assertEqual(result.joint_names, original.joint_names) + np.testing.assert_array_equal(result.world_from_source, original.world_from_source) + for name, episode in original.episodes.items(): + stretched = result.episodes[name] + np.testing.assert_array_equal(stretched.time, episode.time * 80) + for field in ("wrist_position", "wrist_quaternion", "joint_position", "valid"): + np.testing.assert_array_equal(getattr(stretched, field), getattr(episode, field)) + self.assertEqual(episode.time[-1], 2) + destination = Path(self.directory.name) / "stretched.hdf5" + publish_validated(destination, result, self.manifest) + self.assertEqual(load(destination, self.manifest).episodes["demo_000000"].time[-1], 160) + with self.assertRaises(FileExistsError): + publish_validated(destination, result, self.manifest) + self.assertEqual(self.path.read_bytes(), source_bytes) + for name, episode in original.episodes.items(): + query = np.linspace(0, episode.time[-1], 401) + expected = sample(episode, query) + actual = sample(result.episodes[name], query * 80) + for field in ("wrist_position", "wrist_quaternion", "joint_position"): + np.testing.assert_allclose(getattr(actual, field), getattr(expected, field), rtol=0, atol=1e-7) + + def test_stretch_time_rejects_nonfinite_acceleration_and_overflow(self): + original = load(self.path, self.manifest) + for factor in (0, -1, 0.5, float("nan"), float("inf"), np.finfo(float).max): + with self.subTest(factor=factor), self.assertRaises(ContractError): + stretch_time(original, factor) + + def test_stretch_time_cli_identity_and_provenance(self): + manifest_path = Path(self.directory.name) / "manifest.json" + manifest_path.write_text(json.dumps(self.manifest)) + destination = Path(self.directory.name) / "stretched.hdf5" + command = [ + sys.executable, + "-m", + "dex_workbench_tracking.cli", + "stretch-time", + str(self.path), + "--manifest", + str(manifest_path), + "--factor", + "80", + "--output", + str(destination), + ] + result = subprocess.run(command, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("stretch input HDF5 SHA256=", load(destination, self.manifest).metadata["source_description"]) + self.assertNotEqual(subprocess.run(command, capture_output=True).returncode, 0) + bad_destination = Path(self.directory.name) / "bad.hdf5" + command[-1] = str(bad_destination) + self.edit(lambda file: file.attrs.__setitem__("asset_sha256", "b" * 64)) + self.assertNotEqual(subprocess.run(command, capture_output=True).returncode, 0) + self.assertFalse(bad_destination.exists()) + def test_load_roundtrip(self): data = load(self.path, self.manifest) self.assertEqual(data.joint_names, ("finger_a", "finger_b")) @@ -68,7 +130,7 @@ class TrackingTrajectoryTests(unittest.TestCase): for name, bad in ( ("schema_version", "v2"), ("embodiment", "L30"), - ("hand_side", "right"), + ("hand_side", "both"), ("provenance", "unknown"), ("asset_sha256", "xyz"), ("root_link", ""), @@ -274,7 +336,7 @@ class TrackingTrajectoryTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(json.loads(result.stdout)["asset_compatibility"], "NOT_CHECKED") self.edit(lambda file: file.attrs.__setitem__("hand_side", "right")) - result = self.run_cli("validate", self.path) + result = self.run_cli("validate", self.path, "--manifest", manifest) self.assertNotEqual(result.returncode, 0) self.assertIn("FAIL", result.stderr)